From 9447ddb94ffcd0204872277af8911ca167fc8bdc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 20 Jan 2020 12:17:11 -0800 Subject: [PATCH] initial changes to add config services to coretk gui --- daemon/core/gui/coreclient.py | 7 + .../core/gui/dialogs/configserviceconfig.py | 494 ++++++++++++++++++ daemon/core/gui/dialogs/nodeconfigservice.py | 169 ++++++ daemon/core/gui/graph/node.py | 10 + daemon/proto/core/api/grpc/core.proto | 1 + 5 files changed, 681 insertions(+) create mode 100644 daemon/core/gui/dialogs/configserviceconfig.py create mode 100644 daemon/core/gui/dialogs/nodeconfigservice.py diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index f4371715..1e44715a 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -63,6 +63,7 @@ class CoreClient: self.app = app self.master = app.master self.services = {} + self.config_services = {} self.default_services = {} self.emane_models = [] self.observer = None @@ -413,6 +414,12 @@ class CoreClient: group_services = self.services.setdefault(service.group, set()) group_services.add(service.name) + # get config service informations + response = self.client.get_config_services() + for service in response.services: + group_services = self.config_services.setdefault(service.group, set()) + group_services.add(service.name) + # if there are no sessions, create a new session, else join a session response = self.client.get_sessions() logging.info("current sessions: %s", response) diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py new file mode 100644 index 00000000..b017212b --- /dev/null +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -0,0 +1,494 @@ +""" +Service configuration dialog +""" +import tkinter as tk +from tkinter import ttk +from typing import TYPE_CHECKING, Any, List + +import grpc + +from core.api.grpc import core_pb2 +from core.gui.dialogs.copyserviceconfig import CopyServiceConfigDialog +from core.gui.dialogs.dialog import Dialog +from core.gui.errors import show_grpc_error +from core.gui.images import ImageEnum, Images +from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.widgets import CodeText, ListboxScroll + +if TYPE_CHECKING: + from core.gui.app import Application + + +class ConfigServiceConfigDialog(Dialog): + def __init__( + self, master: Any, app: "Application", service_name: str, node_id: int + ): + title = f"{service_name} Config Service" + super().__init__(master, app, title, modal=True) + self.master = master + self.app = app + self.core = app.core + self.node_id = node_id + self.service_name = service_name + self.service_configs = app.core.service_configs + self.file_configs = app.core.file_configs + + self.radiovar = tk.IntVar() + self.radiovar.set(2) + self.filenames = [] + self.dependencies = [] + self.executables = [] + self.startup_commands = [] + self.validation_commands = [] + self.shutdown_commands = [] + self.default_startup = [] + self.default_validate = [] + self.default_shutdown = [] + self.validation_mode = None + self.validation_time = None + self.validation_period = None + self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW, 16) + self.editdelete_img = Images.get(ImageEnum.EDITDELETE, 16) + + self.notebook = None + self.filename_combobox = None + self.startup_commands_listbox = None + self.shutdown_commands_listbox = None + self.validate_commands_listbox = None + self.validation_time_entry = None + self.validation_mode_entry = None + self.service_file_data = None + self.validation_period_entry = None + self.original_service_files = {} + self.temp_service_files = {} + self.modified_files = set() + self.load() + self.draw() + + def load(self): + try: + self.app.core.create_nodes_and_links() + default_config = self.app.core.get_node_service( + self.node_id, self.service_name + ) + self.default_startup = default_config.startup[:] + self.default_validate = default_config.validate[:] + self.default_shutdown = default_config.shutdown[:] + custom_configs = self.service_configs + if ( + self.node_id in custom_configs + and self.service_name in custom_configs[self.node_id] + ): + service_config = custom_configs[self.node_id][self.service_name] + else: + service_config = default_config + + self.dependencies = service_config.dependencies[:] + self.executables = service_config.executables[:] + self.filenames = service_config.configs[:] + self.startup_commands = service_config.startup[:] + self.validation_commands = service_config.validate[:] + self.shutdown_commands = service_config.shutdown[:] + self.validation_mode = service_config.validation_mode + self.validation_time = service_config.validation_timer + self.original_service_files = { + x: self.app.core.get_node_service_file( + self.node_id, self.service_name, x + ) + for x in self.filenames + } + self.temp_service_files = dict(self.original_service_files) + file_configs = self.file_configs + if ( + self.node_id in file_configs + and self.service_name in file_configs[self.node_id] + ): + for file, data in file_configs[self.node_id][self.service_name].items(): + self.temp_service_files[file] = data + except grpc.RpcError as e: + show_grpc_error(e) + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) + + # draw notebook + self.notebook = ttk.Notebook(self.top) + self.notebook.grid(sticky="nsew", pady=PADY) + self.draw_tab_files() + self.draw_tab_directories() + self.draw_tab_startstop() + self.draw_tab_configuration() + + self.draw_buttons() + + def draw_tab_files(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Files") + + label = ttk.Label( + tab, text="Config files and scripts that are generated for this service." + ) + label.grid() + + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="File Name") + label.grid(row=0, column=0, padx=PADX, sticky="w") + self.filename_combobox = ttk.Combobox( + frame, values=self.filenames, state="readonly" + ) + self.filename_combobox.bind( + "<>", self.display_service_file_data + ) + self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, image=self.documentnew_img, state="disabled") + button.bind("", self.add_filename) + button.grid(row=0, column=2, padx=PADX) + button = ttk.Button(frame, image=self.editdelete_img, state="disabled") + button.bind("", self.delete_filename) + button.grid(row=0, column=3) + + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + button = ttk.Radiobutton( + frame, + variable=self.radiovar, + text="Copy Source File", + value=1, + state=tk.DISABLED, + ) + button.grid(row=0, column=0, sticky="w", padx=PADX) + entry = ttk.Entry(frame, state=tk.DISABLED) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) + image = Images.get(ImageEnum.FILEOPEN, 16) + button = ttk.Button(frame, image=image) + button.image = image + button.grid(row=0, column=2) + + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(0, weight=1) + button = ttk.Radiobutton( + frame, + variable=self.radiovar, + text="Use text below for file contents", + value=2, + ) + button.grid(row=0, column=0, sticky="ew") + image = Images.get(ImageEnum.FILEOPEN, 16) + button = ttk.Button(frame, image=image) + button.image = image + button.grid(row=0, column=1) + image = Images.get(ImageEnum.DOCUMENTSAVE, 16) + button = ttk.Button(frame, image=image) + button.image = image + button.grid(row=0, column=2) + + self.service_file_data = CodeText(tab) + self.service_file_data.grid(sticky="nsew") + tab.rowconfigure(self.service_file_data.grid_info()["row"], weight=1) + if len(self.filenames) > 0: + self.filename_combobox.current(0) + self.service_file_data.text.delete(1.0, "end") + self.service_file_data.text.insert( + "end", self.temp_service_files[self.filenames[0]] + ) + self.service_file_data.text.bind( + "", self.update_temp_service_file_data + ) + + def draw_tab_directories(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Directories") + + label = ttk.Label( + tab, + text="Directories required by this service that are unique for each node.", + ) + label.grid() + + def draw_tab_startstop(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + for i in range(3): + tab.rowconfigure(i, weight=1) + self.notebook.add(tab, text="Startup/Shutdown") + commands = [] + # tab 3 + for i in range(3): + label_frame = None + if i == 0: + label_frame = ttk.LabelFrame( + tab, text="Startup Commands", padding=FRAME_PAD + ) + commands = self.startup_commands + elif i == 1: + label_frame = ttk.LabelFrame( + tab, text="Shutdown Commands", padding=FRAME_PAD + ) + commands = self.shutdown_commands + elif i == 2: + label_frame = ttk.LabelFrame( + tab, text="Validation Commands", padding=FRAME_PAD + ) + commands = self.validation_commands + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(1, weight=1) + label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY) + + frame = ttk.Frame(label_frame) + frame.grid(row=0, column=0, sticky="nsew", pady=PADY) + frame.columnconfigure(0, weight=1) + entry = ttk.Entry(frame, textvariable=tk.StringVar()) + entry.grid(row=0, column=0, stick="ew", padx=PADX) + button = ttk.Button(frame, image=self.documentnew_img) + button.bind("", self.add_command) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, image=self.editdelete_img) + button.grid(row=0, column=2, sticky="ew") + button.bind("", self.delete_command) + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.listbox.bind("<>", self.update_entry) + for command in commands: + listbox_scroll.listbox.insert("end", command) + listbox_scroll.listbox.config(height=4) + listbox_scroll.grid(row=1, column=0, sticky="nsew") + if i == 0: + self.startup_commands_listbox = listbox_scroll.listbox + elif i == 1: + self.shutdown_commands_listbox = listbox_scroll.listbox + elif i == 2: + self.validate_commands_listbox = listbox_scroll.listbox + + def draw_tab_configuration(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Configuration", sticky="nsew") + + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + + label = ttk.Label(frame, text="Validation Time") + label.grid(row=0, column=0, sticky="w", padx=PADX) + self.validation_time_entry = ttk.Entry(frame) + self.validation_time_entry.insert("end", self.validation_time) + self.validation_time_entry.config(state=tk.DISABLED) + self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY) + + label = ttk.Label(frame, text="Validation Mode") + label.grid(row=1, column=0, sticky="w", padx=PADX) + if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING: + mode = "BLOCKING" + elif self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING: + mode = "NON_BLOCKING" + else: + mode = "TIMER" + self.validation_mode_entry = ttk.Entry( + frame, textvariable=tk.StringVar(value=mode) + ) + self.validation_mode_entry.insert("end", mode) + self.validation_mode_entry.config(state=tk.DISABLED) + self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY) + + label = ttk.Label(frame, text="Validation Period") + label.grid(row=2, column=0, sticky="w", padx=PADX) + self.validation_period_entry = ttk.Entry( + frame, state=tk.DISABLED, textvariable=tk.StringVar() + ) + self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY) + + label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD) + label_frame.grid(sticky="nsew", pady=PADY) + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.grid(sticky="nsew") + tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) + for executable in self.executables: + listbox_scroll.listbox.insert("end", executable) + + label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD) + label_frame.grid(sticky="nsew", pady=PADY) + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.grid(sticky="nsew") + tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) + for dependency in self.dependencies: + listbox_scroll.listbox.insert("end", dependency) + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(4): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Defaults", command=self.click_defaults) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Copy...", command=self.click_copy) + button.grid(row=0, column=2, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=3, sticky="ew") + + def add_filename(self, event: tk.Event): + # not worry about it for now + return + frame_contains_button = event.widget.master + combobox = frame_contains_button.grid_slaves(row=0, column=1)[0] + filename = combobox.get() + if filename not in combobox["values"]: + combobox["values"] += (filename,) + + def delete_filename(self, event: tk.Event): + # not worry about it for now + return + frame_comntains_button = event.widget.master + combobox = frame_comntains_button.grid_slaves(row=0, column=1)[0] + filename = combobox.get() + if filename in combobox["values"]: + combobox["values"] = tuple([x for x in combobox["values"] if x != filename]) + combobox.set("") + + def add_command(self, event: tk.Event): + frame_contains_button = event.widget.master + listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox + command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get() + if command_to_add == "": + return + for cmd in listbox.get(0, tk.END): + if cmd == command_to_add: + return + listbox.insert(tk.END, command_to_add) + + def update_entry(self, event: tk.Event): + listbox = event.widget + current_selection = listbox.curselection() + if len(current_selection) > 0: + cmd = listbox.get(current_selection[0]) + entry = listbox.master.master.grid_slaves(row=0, column=0)[0].grid_slaves( + row=0, column=0 + )[0] + entry.delete(0, "end") + entry.insert(0, cmd) + + def delete_command(self, event: tk.Event): + button = event.widget + frame_contains_button = button.master + listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox + current_selection = listbox.curselection() + if len(current_selection) > 0: + listbox.delete(current_selection[0]) + entry = frame_contains_button.grid_slaves(row=0, column=0)[0] + entry.delete(0, tk.END) + + def click_apply(self): + current_listbox = self.master.current.listbox + if not self.is_custom_service_config() and not self.is_custom_service_file(): + if self.node_id in self.service_configs: + self.service_configs[self.node_id].pop(self.service_name, None) + current_listbox.itemconfig(current_listbox.curselection()[0], bg="") + self.destroy() + return + + try: + if self.is_custom_service_config(): + startup_commands = self.startup_commands_listbox.get(0, "end") + shutdown_commands = self.shutdown_commands_listbox.get(0, "end") + validate_commands = self.validate_commands_listbox.get(0, "end") + config = self.core.set_node_service( + self.node_id, + self.service_name, + startup_commands, + validate_commands, + shutdown_commands, + ) + if self.node_id not in self.service_configs: + self.service_configs[self.node_id] = {} + self.service_configs[self.node_id][self.service_name] = config + + for file in self.modified_files: + if self.node_id not in self.file_configs: + self.file_configs[self.node_id] = {} + if self.service_name not in self.file_configs[self.node_id]: + self.file_configs[self.node_id][self.service_name] = {} + self.file_configs[self.node_id][self.service_name][ + file + ] = self.temp_service_files[file] + + self.app.core.set_node_service_file( + self.node_id, self.service_name, file, self.temp_service_files[file] + ) + all_current = current_listbox.get(0, tk.END) + current_listbox.itemconfig(all_current.index(self.service_name), bg="green") + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() + + def display_service_file_data(self, event: tk.Event): + combobox = event.widget + filename = combobox.get() + self.service_file_data.text.delete(1.0, "end") + self.service_file_data.text.insert("end", self.temp_service_files[filename]) + + def update_temp_service_file_data(self, event: tk.Event): + scrolledtext = event.widget + filename = self.filename_combobox.get() + self.temp_service_files[filename] = scrolledtext.get(1.0, "end") + if self.temp_service_files[filename] != self.original_service_files[filename]: + self.modified_files.add(filename) + else: + self.modified_files.discard(filename) + + def is_custom_service_config(self): + startup_commands = self.startup_commands_listbox.get(0, "end") + shutdown_commands = self.shutdown_commands_listbox.get(0, "end") + validate_commands = self.validate_commands_listbox.get(0, "end") + return ( + set(self.default_startup) != set(startup_commands) + or set(self.default_validate) != set(validate_commands) + or set(self.default_shutdown) != set(shutdown_commands) + ) + + def is_custom_service_file(self): + return len(self.modified_files) > 0 + + def click_defaults(self): + if self.node_id in self.service_configs: + self.service_configs[self.node_id].pop(self.service_name, None) + if self.node_id in self.file_configs: + self.file_configs[self.node_id].pop(self.service_name, None) + self.temp_service_files = dict(self.original_service_files) + filename = self.filename_combobox.get() + self.service_file_data.text.delete(1.0, "end") + self.service_file_data.text.insert("end", self.temp_service_files[filename]) + self.startup_commands_listbox.delete(0, tk.END) + self.validate_commands_listbox.delete(0, tk.END) + self.shutdown_commands_listbox.delete(0, tk.END) + for cmd in self.default_startup: + self.startup_commands_listbox.insert(tk.END, cmd) + for cmd in self.default_validate: + self.validate_commands_listbox.insert(tk.END, cmd) + for cmd in self.default_shutdown: + self.shutdown_commands_listbox.insert(tk.END, cmd) + + def click_copy(self): + dialog = CopyServiceConfigDialog(self, self.app, self.node_id) + dialog.show() + + def append_commands( + self, commands: List[str], listbox: tk.Listbox, to_add: List[str] + ): + for cmd in to_add: + commands.append(cmd) + listbox.insert(tk.END, cmd) diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py new file mode 100644 index 00000000..46897d60 --- /dev/null +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -0,0 +1,169 @@ +""" +core node services +""" +import tkinter as tk +from tkinter import messagebox, ttk +from typing import TYPE_CHECKING, Any, Set + +from core.gui.dialogs.configserviceconfig import ConfigServiceConfigDialog +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import FRAME_PAD, PADX, PADY +from core.gui.widgets import CheckboxList, ListboxScroll + +if TYPE_CHECKING: + from core.gui.app import Application + from core.gui.graph.node import CanvasNode + + +class NodeConfigServiceDialog(Dialog): + def __init__( + self, + master: Any, + app: "Application", + canvas_node: "CanvasNode", + services: Set[str] = None, + ): + title = f"{canvas_node.core_node.name} Config Services" + super().__init__(master, app, title, modal=True) + self.app = app + self.canvas_node = canvas_node + self.node_id = canvas_node.core_node.id + self.groups = None + self.services = None + self.current = None + if services is None: + services = set(canvas_node.core_node.config_services) + self.current_services = services + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + frame = ttk.Frame(self.top) + frame.grid(stick="nsew", pady=PADY) + frame.rowconfigure(0, weight=1) + for i in range(3): + frame.columnconfigure(i, weight=1) + label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD) + label_frame.grid(row=0, column=0, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.groups = ListboxScroll(label_frame) + self.groups.grid(sticky="nsew") + for group in sorted(self.app.core.config_services): + self.groups.listbox.insert(tk.END, group) + self.groups.listbox.bind("<>", self.handle_group_change) + self.groups.listbox.selection_set(0) + + label_frame = ttk.LabelFrame(frame, text="Services") + label_frame.grid(row=0, column=1, sticky="nsew") + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) + self.services = CheckboxList( + label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD + ) + self.services.grid(sticky="nsew") + + label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) + label_frame.grid(row=0, column=2, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.current = ListboxScroll(label_frame) + self.current.grid(sticky="nsew") + for service in sorted(self.current_services): + self.current.listbox.insert(tk.END, service) + if self.is_custom_service(service): + self.current.listbox.itemconfig(tk.END, bg="green") + + frame = ttk.Frame(self.top) + frame.grid(stick="ew") + for i in range(4): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Configure", command=self.click_configure) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Save", command=self.click_save) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Remove", command=self.click_remove) + button.grid(row=0, column=2, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Cancel", command=self.click_cancel) + button.grid(row=0, column=3, sticky="ew") + + # trigger group change + self.groups.listbox.event_generate("<>") + + def handle_group_change(self, event: tk.Event = None): + selection = self.groups.listbox.curselection() + if selection: + index = selection[0] + group = self.groups.listbox.get(index) + self.services.clear() + for name in sorted(self.app.core.config_services[group]): + checked = name in self.current_services + self.services.add(name, checked) + + def service_clicked(self, name: str, var: tk.IntVar): + if var.get() and name not in self.current_services: + self.current_services.add(name) + elif not var.get() and name in self.current_services: + self.current_services.remove(name) + self.current.listbox.delete(0, tk.END) + for name in sorted(self.current_services): + self.current.listbox.insert(tk.END, name) + if self.is_custom_service(name): + self.current.listbox.itemconfig(tk.END, bg="green") + self.canvas_node.core_node.config_services[:] = self.current_services + + def click_configure(self): + current_selection = self.current.listbox.curselection() + if len(current_selection): + dialog = ConfigServiceConfigDialog( + master=self, + app=self.app, + service_name=self.current.listbox.get(current_selection[0]), + node_id=self.node_id, + ) + dialog.show() + else: + messagebox.showinfo( + "Node service configuration", "Select a service to configure" + ) + + def click_save(self): + if ( + self.current_services + != self.app.core.default_services[self.canvas_node.core_node.model] + ): + self.canvas_node.core_node.config_services[:] = self.current_services + else: + if len(self.canvas_node.core_node.config_services) > 0: + self.canvas_node.core_node.config_services[:] = [] + self.destroy() + + def click_cancel(self): + self.current_services = None + self.destroy() + + def click_remove(self): + cur = self.current.listbox.curselection() + if cur: + service = self.current.listbox.get(cur[0]) + self.current.listbox.delete(cur[0]) + self.current_services.remove(service) + for checkbutton in self.services.frame.winfo_children(): + if checkbutton["text"] == service: + checkbutton.invoke() + return + + def is_custom_service(self, service: str) -> bool: + service_configs = self.app.core.service_configs + file_configs = self.app.core.file_configs + if self.node_id in service_configs and service in service_configs[self.node_id]: + return True + if ( + self.node_id in file_configs + and service in file_configs[self.node_id] + and file_configs[self.node_id][service] + ): + return True + return False diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index c1d8e075..e5974625 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -10,6 +10,7 @@ from core.gui import themes from core.gui.dialogs.emaneconfig import EmaneConfigDialog from core.gui.dialogs.mobilityconfig import MobilityConfigDialog from core.gui.dialogs.nodeconfig import NodeConfigDialog +from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog from core.gui.dialogs.nodeservice import NodeServiceDialog from core.gui.dialogs.wlanconfig import WlanConfigDialog from core.gui.errors import show_grpc_error @@ -180,6 +181,7 @@ class CanvasNode: context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): context.add_command(label="Services", state=tk.DISABLED) + context.add_command(label="Config Services", state=tk.DISABLED) if is_wlan: context.add_command(label="WLAN Config", command=self.show_wlan_config) if is_wlan and self.core_node.id in self.app.core.mobility_players: @@ -198,6 +200,9 @@ class CanvasNode: context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): context.add_command(label="Services", command=self.show_services) + context.add_command( + label="Config Services", command=self.show_config_services + ) if is_emane: context.add_command( label="EMANE Config", command=self.show_emane_config @@ -253,6 +258,11 @@ class CanvasNode: dialog = NodeServiceDialog(self.app.master, self.app, self) dialog.show() + def show_config_services(self): + self.canvas.context = None + dialog = NodeConfigServiceDialog(self.app.master, self.app, self) + dialog.show() + def has_emane_link(self, interface_id: int) -> core_pb2.Node: result = None for edge in self.edges: diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 215a23d5..93435077 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -953,6 +953,7 @@ message Node { string opaque = 9; string image = 10; string server = 11; + repeated string config_services = 12; } message Link {