Compare commits

...

19 Commits

Author SHA1 Message Date
grossmj
fefd1097de Release v3.0.4 2025-02-25 18:24:32 +08:00
grossmj
4c35ea7f17 Upgrade dependencies 2025-02-22 21:10:34 +10:00
grossmj
200fbc533b Fix auto idle-pc for IOS templates 2025-02-22 20:14:19 +10:00
grossmj
c94f63e636 Add user info and password change for logged-in user. Fixes #3698 2025-02-19 18:26:08 +10:00
grossmj
f53124f09a Development on 3.0.4 2025-01-23 13:31:37 +10:00
grossmj
d7a51ed588 Release v3.0.3 2025-01-22 19:06:43 +10:00
grossmj
dbca1b7106 Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3/crash_report.py
#	gns3/version.py
2025-01-22 18:37:46 +10:00
grossmj
15e5cac33b Set minimum duration for progress dialog when uploading. Ref https://github.com/GNS3/gns3-gui/issues/3682 2025-01-21 16:35:11 +10:00
grossmj
342ca95bd2 Release v2.2.53 2025-01-21 11:52:12 +10:00
grossmj
8191a51b2b Add logs when uploading images to the controller 2025-01-20 11:39:19 +10:00
grossmj
7d112551a8 Option to disable SSL certificate verification for future connections. Fixes https://github.com/GNS3/gns3-gui/issues/3694 2025-01-19 13:23:06 +10:00
grossmj
b5e867f2cd Fix packet capture when connected to a controller with SSL. Fixes https://github.com/GNS3/gns3-gui/issues/3696 2025-01-19 10:49:05 +10:00
grossmj
e5632e565d Upgrade pytest to the latest version 2025-01-17 18:47:36 +10:00
grossmj
bd785bf6cd Update status after importing an image when installing a new appliance. Fixes #3691 2025-01-07 17:53:52 +07:00
grossmj
2ae788a8f5 Update file browser filters to find IOU images without extension. Fixes #3692 2025-01-07 11:36:48 +07:00
grossmj
d7c1754323 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3/main_window.py
#	gns3/modules/traceng/pages/traceng_preferences_page.py
2025-01-07 11:33:41 +07:00
grossmj
187ef561fd Update file browser filters for all files and IOU images 2025-01-07 11:32:17 +07:00
grossmj
ad2ce4cfef Development on 3.0.3.dev1 2025-01-07 11:12:06 +07:00
grossmj
97070718fa Upgrade dependencies 2024-12-30 10:49:40 +07:00
35 changed files with 93759 additions and 92779 deletions

View File

@@ -1,5 +1,27 @@
# Change Log
## 3.0.4 25/02/2025
* Upgrade dependencies
* Fix auto idle-pc for IOS templates
* Add user info and password change for logged-in user. Fixes #3698
## 3.0.3 22/01/2025
* Set minimum duration for progress dialog when uploading. Ref https://github.com/GNS3/gns3-gui/issues/3682
* Add logs when uploading images to the controller
* Option to disable SSL certificate verification for future connections. Fixes https://github.com/GNS3/gns3-gui/issues/3694
* Fix packet capture when connected to a controller with SSL. Fixes https://github.com/GNS3/gns3-gui/issues/3696
* Update status after importing an image when installing a new appliance. Fixes #3691
* Update file browser filters to find IOU images without extension. Fixes #3692
* Upgrade dependencies
## 2.2.53 21/01/2025
* Update file browser filters for all files and IOU images
* Upgrade dependencies
* Fix Linux Mint default terminal configuration
## 3.0.2 03/01/2025
* Add button to create templates based on images that are not used by any yet.

View File

@@ -1,2 +1,2 @@
pytest==8.3.2
pytest==8.3.4
pytest-timeout==2.3.1

View File

@@ -50,7 +50,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "https://142c803f12d32e781a654ef31138c684@o19455.ingest.us.sentry.io/38506"
DSN = "https://3a078102c42520fc0bd28a3d8aeb1a97@o19455.ingest.us.sentry.io/38506"
_instance = None
def __init__(self):

View File

@@ -522,6 +522,12 @@ Usage: {}
image_upload_manger = ImageUploadManager(image, Controller.instance(), self.parent())
image_upload_manger.upload()
# refresh the images list
if Controller.instance().isRemote() or self._compute_id != "local":
self._registry.getRemoteImageList()
else:
self.images_changed_signal.emit()
def _install(self, version):
"""
Install the appliance in GNS3

View File

@@ -55,13 +55,13 @@ class ImageDialog(QtWidgets.QDialog, Ui_ImageDialog):
self,
"Select one or more images to upload",
QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DownloadLocation),
"Images (*.bin *.image *.iol *.qcow2 *.vmdk *.iso);;All files (*.*)"
"Images (*.bin *.image *.iol *.qcow2 *.vmdk *.iso x86_64* i86bi*);;All files (*)"
)
error_msgs = ""
for path in files:
log.debug("Uploading image '{}' to controller".format(path))
image_filename = os.path.basename(path)
install_appliances = self.uiInstallApplianceCheckBox.isChecked()
log.info("Uploading image '{}' to controller".format(image_filename))
try:
Controller.instance().post(
f"/images/upload/{image_filename}",

View File

@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
from ..qt import QtCore, QtGui, QtWidgets
from ..ui.password_dialog_ui import Ui_PasswordDialog
import logging
log = logging.getLogger(__name__)
class PasswordDialog(QtWidgets.QDialog, Ui_PasswordDialog):
"""
Password dialog.
"""
def __init__(self, parent):
"""
:param parent: parent widget.
"""
super().__init__(parent)
self.setupUi(self)
self._password = None
self._eye_on_icon = QtGui.QIcon(':/icons/eye-on.svg')
self._eye_off_icon = QtGui.QIcon(':/icons/eye-off.svg')
for line_edit in [self.uiPasswordLineEdit, self.uiConfirmPasswordLineEdit]:
action = line_edit.addAction(self._eye_on_icon, QtWidgets.QLineEdit.TrailingPosition)
button = action.associatedWidgets()[-1]
button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
button.pressed.connect(self.onPressedSlot)
#button.released.connect(self.onReleasedSlot)
def onPressedSlot(self):
button = self.sender()
line_edit = button.parent()
if line_edit.echoMode() == QtWidgets.QLineEdit.Password:
button.setIcon(self._eye_off_icon)
line_edit.setEchoMode(QtWidgets.QLineEdit.Normal)
else:
button.setIcon(self._eye_on_icon)
line_edit.setEchoMode(QtWidgets.QLineEdit.Password)
# def onReleasedSlot(self):
#
# button = self.sender()
# button.setIcon(self._eye_on_icon)
# button.parent().setEchoMode(QtWidgets.QLineEdit.Password)
def getPassword(self):
return self._password
def done(self, result):
if result:
new_password = self.uiPasswordLineEdit.text()
confirm_password = self.uiConfirmPasswordLineEdit.text()
if new_password != confirm_password:
QtWidgets.QMessageBox.critical(self, "Error", "Passwords do not match.")
return
pattern = re.compile(r'^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8}$')
if not pattern.match(new_password):
QtWidgets.QMessageBox.critical(self, "Error", "Password must be at least 8 characters long and contain at least one digit, one lowercase letter and one uppercase letter.")
return
self._password = new_password
super().done(result)

View File

@@ -24,6 +24,7 @@ from ..ui.preferences_dialog_ui import Ui_PreferencesDialog
from ..pages.controller_preferences_page import ControllerPreferencesPage
from ..pages.general_preferences_page import GeneralPreferencesPage
from ..pages.packet_capture_preferences_page import PacketCapturePreferencesPage
from ..pages.user_preferences_page import UserPreferencesPage
from ..pages.gns3_vm_preferences_page import GNS3VMPreferencesPage
from ..modules import MODULES
@@ -85,8 +86,9 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
pages = [
GeneralPreferencesPage,
ControllerPreferencesPage,
UserPreferencesPage,
#GNS3VMPreferencesPage,
PacketCapturePreferencesPage,
PacketCapturePreferencesPage
]
for page in pages:

View File

@@ -83,7 +83,7 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
filter = ""
if sys.platform.startswith("win"):
filter = "Executable (*.exe);;All files (*.*)"
filter = "Executable (*.exe);;All files (*)"
server_path = shutil.which("gns3server")
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select the local server", server_path, filter)
if not path:
@@ -204,7 +204,8 @@ class SetupWizard(QtWidgets.QWizard, Ui_SetupWizard):
remote_controller_settings["remote"] = True
remote_controller_settings["host"] = self.uiRemoteMainServerHostLineEdit.text()
remote_controller_settings["port"] = self.uiRemoteMainServerPortSpinBox.value()
remote_controller_settings["protocol"] = "http"
remote_controller_settings["protocol"] = self.uiRemoteMainServerProtocolComboBox.currentText().lower()
remote_controller_settings["accept_insecure_ssl_certificate"] = False
remote_controller_settings["username"] = self.uiRemoteMainServerUserLineEdit.text()
remote_controller_settings["password"] = self.uiRemoteMainServerPasswordLineEdit.text()
Controller.instance().setSettings(remote_controller_settings)

View File

@@ -184,7 +184,7 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
def _symbolBrowserSlot(self):
# supported image file formats
file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.pbm *.pgm *.png *.ppm *.xbm *.xpm *.gif);;All files (*.*)"
file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.pbm *.pgm *.png *.ppm *.xbm *.xpm *.gif);;All files (*)"
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Image", SymbolSelectionDialog._symbols_dir, file_formats)
if not path:
return

View File

@@ -1281,7 +1281,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
path, _ = QtWidgets.QFileDialog.getOpenFileName(self,
"Import {}".format(os.path.basename(config_file)),
self._import_config_directory,
"All files (*.*);;Config files (*.cfg)",
"All files (*);;Config files (*.cfg)",
"Config files (*.cfg)")
if not path:
continue
@@ -1328,7 +1328,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
for item in items:
for config_file in item.node().configFiles():
path, ok = QtWidgets.QFileDialog.getSaveFileName(self, "Export file", os.path.join(self._export_config_directory, item.node().name() + "_" + os.path.basename(config_file)), "All files (*.*);;Config files (*.cfg)")
path, ok = QtWidgets.QFileDialog.getSaveFileName(self, "Export file", os.path.join(self._export_config_directory, item.node().name() + "_" + os.path.basename(config_file)), "All files (*);;Config files (*.cfg)")
if not path:
continue
self._export_config_directory = os.path.dirname(path)

View File

@@ -67,6 +67,7 @@ class QNetworkReplyWatcher(QtCore.QObject):
self._progress = QtWidgets.QProgressDialog(progress_text, "Cancel", 0, 0, parent)
self._progress.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
self._progress.setWindowModality(QtCore.Qt.ApplicationModal)
self._progress.setMinimumDuration(0)
else:
self._progress = None
@@ -102,11 +103,11 @@ class QNetworkReplyWatcher(QtCore.QObject):
reply.finished.connect(loop.quit)
if self._progress:
reply.finished.connect(self._progress.close)
if uploading:
reply.uploadProgress.connect(self._updateProgress)
else:
reply.downloadProgress.connect(self._updateProgress)
reply.finished.connect(self._progress.close)
self._progress.canceled.connect(reply.abort)
self._progress.show()
@@ -158,7 +159,7 @@ class HTTPClient(QtCore.QObject):
self._retry_connection = False
self._connected = False
self._shutdown = False # shutdown in progress
self._accept_insecure_certificate = settings.get("accept_insecure_certificate", False)
self._accept_insecure_ssl_certificate = settings.get("accept_insecure_ssl_certificate", False)
# Add custom CA
# ssl_config = QtNetwork.QSslConfiguration.defaultConfiguration()
@@ -224,12 +225,12 @@ class HTTPClient(QtCore.QObject):
return self._protocol
def setAcceptInsecureCertificate(self, certificate: bool) -> None:
def setAcceptInsecureCertificate(self, accept_insecure_ssl_certificate: bool) -> None:
"""
Does the server accept this insecure SSL certificate digest
"""
self._accept_insecure_certificate = certificate
self._accept_insecure_ssl_certificate = accept_insecure_ssl_certificate
def url(self) -> str:
"""
@@ -251,6 +252,13 @@ class HTTPClient(QtCore.QObject):
return "{}://{}:{}".format(self.protocol(), host, self.port())
def getToken(self):
"""
Return the JWT token
"""
return self._jwt_token
def shutdown(self) -> None:
"""
Stop the server and stop to accept queries
@@ -545,7 +553,12 @@ class HTTPClient(QtCore.QObject):
self._auth_attempted = True
content = self._executeHTTPQuery("POST", "/access/users/authenticate", body=body, wait=True)
if content:
log.info(f"Authenticated with controller {self._host} on port {self._port}")
additional_ssl_info = ""
if self._protocol == "https":
additional_ssl_info = "using SSL"
if self._accept_insecure_ssl_certificate:
additional_ssl_info += " with insecure certificate"
log.info(f"Authenticated with controller {self._host} on port {self._port} {additional_ssl_info}")
token = content.get("access_token")
if token:
self._auth_attempted = False
@@ -839,7 +852,7 @@ class HTTPClient(QtCore.QObject):
Handle SSL errors
"""
if self._accept_insecure_certificate:
if self._accept_insecure_ssl_certificate:
reply.ignoreSslErrors()
return
@@ -852,7 +865,6 @@ class HTTPClient(QtCore.QObject):
digest = peer_cert.digest()
if host_port_key in self._ssl_exceptions:
if self._ssl_exceptions[host_port_key] == digest:
reply.ignoreSslErrors()
return
@@ -865,6 +877,8 @@ class HTTPClient(QtCore.QObject):
msgbox.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
connect_button = QtWidgets.QPushButton(f"&Connect to {url.host()}:{url.port()}", msgbox)
msgbox.addButton(connect_button, QtWidgets.QMessageBox.YesRole)
checkbox = QtWidgets.QCheckBox("Accept insecure certificate for future connections", parent=msgbox)
msgbox.setCheckBox(checkbox)
abort_button = QtWidgets.QPushButton("&Abort", msgbox)
msgbox.addButton(abort_button, QtWidgets.QMessageBox.RejectRole)
msgbox.setDefaultButton(abort_button)
@@ -873,6 +887,12 @@ class HTTPClient(QtCore.QObject):
if msgbox.clickedButton() == connect_button:
self._ssl_exceptions[host_port_key] = digest
if checkbox.isChecked():
log.warning(f"Accepting insecure SSL certificate for future connections to {host_port_key}")
from gns3.controller import Controller
self._accept_insecure_ssl_certificate = True
controller_settings = {"accept_insecure_ssl_certificate": True}
Controller.instance().setSettings(controller_settings)
reply.ignoreSslErrors()
else:
for error in ssl_errors:

View File

@@ -47,7 +47,7 @@ class ImageUploadManager(object):
def _fileUploadToController(self) -> bool:
log.debug("Uploading image '{}' to controller".format(self._image.path))
log.info("Uploading image '{}' to controller".format(self._image.filename))
try:
self._controller.post(
f"/images/upload/{self._image.filename}",

View File

@@ -121,6 +121,8 @@ class Link(QtCore.QObject):
if self._network_manager is None:
self._network_manager = QtNetwork.QNetworkAccessManager(self)
self._network_manager.sslErrors.connect(Controller.instance().httpClient().handleSslError)
self._response_stream = Controller.instance().get(
"/projects/{project_id}/links/{link_id}/capture/stream".format(project_id=self.project().id(), link_id=self._link_id),
callback=None,

View File

@@ -456,7 +456,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
if not os.path.exists(self._appliance_dir):
directory = Topology.instance().projectsDirPath()
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Import appliance", directory,
"All files (*.*);;GNS3 Appliance (*.gns3appliance *.gns3a)",
"All files (*);;GNS3 Appliance (*.gns3appliance *.gns3a)",
"GNS3 Appliance (*.gns3appliance *.gns3a)")
if path:
self.loadPath(path)
@@ -475,7 +475,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
if self._project_dir is None or not os.path.exists(self._project_dir):
directory = Topology.instance().projectsDirPath()
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open project", directory,
"All files (*.*);;GNS3 Project (*.gns3);;GNS3 Portable Project (*.gns3project *.gns3p);;NET files (*.net)",
"All files (*);;GNS3 Project (*.gns3);;GNS3 Portable Project (*.gns3project *.gns3p);;NET files (*.net)",
"GNS3 Project (*.gns3)")
if path:
self.loadPath(path)
@@ -951,7 +951,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Slot called when inserting an image on the scene.
"""
# supported image file formats
file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.gif *.pbm *.pgm *.png *.ppm *.xbm *.xpm);;All files (*.*)"
file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.gif *.pbm *.pgm *.png *.ppm *.xbm *.xpm);;All files (*)"
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Image", self._pictures_dir, file_formats)
if not path:
@@ -1489,7 +1489,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
if not os.path.exists(directory):
directory = Topology.instance().projectsDirPath()
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open project", directory,
"All files (*.*);;GNS3 Portable Project (*.gns3project *.gns3p)",
"All files (*);;GNS3 Portable Project (*.gns3project *.gns3p)",
"GNS3 Portable Project (*.gns3project *.gns3p)")
if path:
Topology.instance().importProject(path)

View File

@@ -218,15 +218,20 @@ class IOSRouterWizard(VMWithImagesWizard, Ui_IOSRouterWizard):
image = self.uiIOSImageLineEdit.text()
platform = self.uiPlatformComboBox.currentText()
ram = self.uiRamSpinBox.value()
Controller.instance().postCompute("/auto_idlepc",
self._compute_id,
callback=self._computeAutoIdlepcCallback,
timeout=None,
body={
"image": image,
"platform": platform,
"ram": ram},
wait=True)
Controller.instance().postCompute(
"/dynamips/auto_idlepc",
self._compute_id,
callback=self._computeAutoIdlepcCallback,
body={
"image": image,
"platform": platform,
"ram": ram
},
timeout=None,
progress_text="Finding an Idle-PC value...",
wait=True
)
def _etherSwitchSlot(self, state):
"""

View File

@@ -51,7 +51,7 @@ class DynamipsPreferencesPage(QtWidgets.QWidget, Ui_DynamipsPreferencesPageWidge
file_filter = ""
if sys.platform.startswith("win"):
file_filter = "Executable (*.exe);;All files (*.*)"
file_filter = "Executable (*.exe);;All files (*)"
dynamips_path = shutil.which("dynamips")
if sys.platform.startswith("darwin") and dynamips_path is None:

View File

@@ -229,7 +229,7 @@ class IOSRouterPreferencesPage(QtWidgets.QWidget, Ui_IOSRouterPreferencesPageWid
path, _ = QtWidgets.QFileDialog.getOpenFileName(parent,
"Select an IOS image",
cls._default_images_dir,
"All files (*.*);;IOS image (*.bin *.image)",
"All files (*);;IOS image (*.bin *.image)",
"IOS image (*.bin *.image)")
if not path:

View File

@@ -299,8 +299,8 @@ class IOUDevicePreferencesPage(QtWidgets.QWidget, Ui_IOUDevicePreferencesPageWid
path, _ = QtWidgets.QFileDialog.getOpenFileName(parent,
"Select an IOU image",
cls._default_images_dir,
"All file (*);;IOU image (*.bin *.image *.iol)",
"IOU image (*.bin *.image *.iol)")
"All file (*);;IOU image (x86_64* i86bi* *.bin *.image *.iol)",
"IOU image (x86_64* i86bi* *.bin *.image *.iol)")
if not path:
return

View File

@@ -51,7 +51,7 @@ class VPCSPreferencesPage(QtWidgets.QWidget, Ui_VPCSPreferencesPageWidget):
filter = ""
if sys.platform.startswith("win"):
filter = "Executable (*.exe);;All files (*.*)"
filter = "Executable (*.exe);;All files (*)"
vpcs_path = shutil.which("vpcs")
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select VPCS", vpcs_path, filter)
if not path:

View File

@@ -109,7 +109,7 @@ class ControllerPreferencesPage(QtWidgets.QWidget, Ui_ControllerPreferencesPageW
filter = ""
if sys.platform.startswith("win"):
filter = "Executable (*.exe);;All files (*.*)"
filter = "Executable (*.exe);;All files (*)"
server_path = shutil.which("gns3server")
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select the local server", server_path, filter)
if not path:
@@ -124,7 +124,7 @@ class ControllerPreferencesPage(QtWidgets.QWidget, Ui_ControllerPreferencesPageW
filter = ""
if sys.platform.startswith("win"):
filter = "Executable (*.exe);;All files (*.*)"
filter = "Executable (*.exe);;All files (*)"
ubridge_path = shutil.which("ubridge")
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select ubridge executable", ubridge_path, filter)
@@ -286,6 +286,12 @@ class ControllerPreferencesPage(QtWidgets.QWidget, Ui_ControllerPreferencesPageW
"udp_start_port_range": self.uiUDPStartPortSpinBox.value(),
"udp_end_port_range": self.uiUDPEndPortSpinBox.value()})
# reset the accept_insecure_ssl_certificate setting if the host or port has changed
current_host_port_key = f"{local_server_settings['host']}:{local_server_settings['port']}"
new_host_port_key = f"{new_local_server_settings['host']}:{new_local_server_settings['port']}"
if current_host_port_key != new_host_port_key:
new_local_server_settings["accept_insecure_ssl_certificate"] = False
if new_local_server_settings["console_end_port_range"] <= new_local_server_settings["console_start_port_range"]:
QtWidgets.QMessageBox.critical(self, "Port range", "Invalid console port range from {} to {}".format(new_local_server_settings["console_start_port_range"],
new_local_server_settings["console_end_port_range"]))

View File

@@ -195,7 +195,7 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
configuration_file_path = LocalConfig.instance().configFilePath()
directory = os.path.dirname(configuration_file_path)
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Import configuration file", directory, "Configuration file (*.ini *.conf);;All files (*.*)")
path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Import configuration file", directory, "Configuration file (*.ini *.conf);;All files (*)")
if not path:
return
@@ -240,7 +240,7 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
configuration_file_path = LocalConfig.instance().configFilePath()
directory = os.path.dirname(configuration_file_path)
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export configuration file", directory, "Configuration file (*.ini *.conf);;All files (*.*)")
path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export configuration file", directory, "Configuration file (*.ini *.conf);;All files (*)")
if not path:
return

View File

@@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Configuration page for user preferences.
"""
from gns3.qt import QtWidgets, qslot, sip_is_deleted
from ..ui.user_preferences_page_ui import Ui_UserPreferencesPageWidget
from gns3.controller import Controller
from gns3.dialogs.password_dialog import PasswordDialog
import logging
log = logging.getLogger(__name__)
class UserPreferencesPage(QtWidgets.QWidget, Ui_UserPreferencesPageWidget):
"""
QWidget configuration page for user preferences.
"""
def __init__(self, parent=None):
super().__init__()
self.setupUi(self)
self.uiChangePasswordPushButton.clicked.connect(self._changePasswordSlot)
self.uiCopyAccessTokenPushButton.clicked.connect(self._copyAccessTokenSlot)
self.uiChangePasswordPushButton.setEnabled(False)
self.uiCopyAccessTokenPushButton.setEnabled(False)
Controller.instance().connected_signal.connect(self.loadPreferences)
Controller.instance().disconnected_signal.connect(self._disconnectSlot)
def _populateWidgets(self, result):
"""
Populates the widgets with the user information.
:param result: user information
"""
self.uiUsernameLineEdit.setText(result.get("username", "N/A"))
self.uiFullNameLineEdit.setText(result.get("full_name", "N/A"))
self.uiEmailLineEdit.setText(result.get("email", "N/A"))
self.uiChangePasswordPushButton.setEnabled(True)
self.uiCopyAccessTokenPushButton.setEnabled(True)
def _disconnectSlot(self):
"""
Resets the widgets when the controller is disconnected.
"""
self.uiUsernameLineEdit.clear()
self.uiFullNameLineEdit.clear()
self.uiEmailLineEdit.clear()
self.uiChangePasswordPushButton.setEnabled(False)
self.uiCopyAccessTokenPushButton.setEnabled(False)
def loadPreferences(self):
"""
Loads the user preferences.
"""
if Controller.instance().connected():
Controller.instance().get("/access/users/me", self._getUserInfoCallback)
else:
log.error("Cannot load the user information in the preferences dialog: not connected to the controller")
@qslot
def _getUserInfoCallback(self, result, error=False, **kwargs):
"""
Callback to get the user information.
"""
if sip_is_deleted(self):
return
if error:
if "message" in result:
log.error("Error while getting the logged-in user information: {}".format(result["message"]))
return
self._populateWidgets(result)
def savePreferences(self):
"""
Saves the user preferences.
"""
pass
def _changePasswordSlot(self):
"""
Slot to change the user password.
"""
dialog = PasswordDialog(self)
if dialog.exec_():
password = dialog.getPassword()
new_settings = {"password": password}
Controller.instance().put("/access/users/me", self._saveUserSettingsCallback, new_settings)
def _saveUserSettingsCallback(self, result, error=False, **kwargs):
"""
Callback to save the user settings.
"""
if error and "message" in result:
QtWidgets.QMessageBox.critical(
self,
"Save user settings",
"Error while saving user settings: {}".format(result["message"])
)
else:
QtWidgets.QMessageBox.information(self, "Change password", "New password saved successfully")
def _copyAccessTokenSlot(self):
"""
Slot to copy the access token to the clipboard.
"""
if Controller.instance().connected():
token = Controller.instance().httpClient().getToken()
if token:
QtWidgets.QApplication.clipboard().setText(token)
log.info("Access token copied to the clipboard")

View File

@@ -340,6 +340,7 @@ CONTROLLER_SETTINGS = {
"ubridge_path": "ubridge",
"host": DEFAULT_CONTROLLER_HOST,
"port": DEFAULT_CONTROLLER_PORT,
"accept_insecure_ssl_certificate": False,
"images_path": DEFAULT_IMAGES_PATH,
"projects_path": DEFAULT_PROJECTS_PATH,
"appliances_path": DEFAULT_APPLIANCES_PATH,

121
gns3/ui/password_dialog.ui Normal file
View File

@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PasswordDialog</class>
<widget class="QDialog" name="PasswordDialog">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>377</width>
<height>111</height>
</rect>
</property>
<property name="windowTitle">
<string>Change password</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="1">
<widget class="QLineEdit" name="uiConfirmPasswordLineEdit">
<property name="maxLength">
<number>100</number>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="uiPasswordLabel">
<property name="text">
<string>New password:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="uiPasswordLineEdit">
<property name="maxLength">
<number>100</number>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="uiConfirmPasswordLabel">
<property name="text">
<string>Confirm password:</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QDialogButtonBox" name="uiButtonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="3" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<tabstops>
<tabstop>uiPasswordLineEdit</tabstop>
<tabstop>uiConfirmPasswordLineEdit</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>uiButtonBox</sender>
<signal>accepted()</signal>
<receiver>PasswordDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>uiButtonBox</sender>
<signal>rejected()</signal>
<receiver>PasswordDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/password_dialog.ui'
#
# Created by: PyQt5 UI code generator 5.15.10
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_PasswordDialog(object):
def setupUi(self, PasswordDialog):
PasswordDialog.setObjectName("PasswordDialog")
PasswordDialog.setWindowModality(QtCore.Qt.ApplicationModal)
PasswordDialog.resize(377, 111)
PasswordDialog.setModal(True)
self.gridLayout = QtWidgets.QGridLayout(PasswordDialog)
self.gridLayout.setObjectName("gridLayout")
self.uiConfirmPasswordLineEdit = QtWidgets.QLineEdit(PasswordDialog)
self.uiConfirmPasswordLineEdit.setMaxLength(100)
self.uiConfirmPasswordLineEdit.setEchoMode(QtWidgets.QLineEdit.Password)
self.uiConfirmPasswordLineEdit.setObjectName("uiConfirmPasswordLineEdit")
self.gridLayout.addWidget(self.uiConfirmPasswordLineEdit, 1, 1, 1, 1)
self.uiPasswordLabel = QtWidgets.QLabel(PasswordDialog)
self.uiPasswordLabel.setObjectName("uiPasswordLabel")
self.gridLayout.addWidget(self.uiPasswordLabel, 0, 0, 1, 1)
self.uiPasswordLineEdit = QtWidgets.QLineEdit(PasswordDialog)
self.uiPasswordLineEdit.setMaxLength(100)
self.uiPasswordLineEdit.setEchoMode(QtWidgets.QLineEdit.Password)
self.uiPasswordLineEdit.setObjectName("uiPasswordLineEdit")
self.gridLayout.addWidget(self.uiPasswordLineEdit, 0, 1, 1, 1)
self.uiConfirmPasswordLabel = QtWidgets.QLabel(PasswordDialog)
self.uiConfirmPasswordLabel.setObjectName("uiConfirmPasswordLabel")
self.gridLayout.addWidget(self.uiConfirmPasswordLabel, 1, 0, 1, 1)
self.uiButtonBox = QtWidgets.QDialogButtonBox(PasswordDialog)
self.uiButtonBox.setOrientation(QtCore.Qt.Horizontal)
self.uiButtonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
self.uiButtonBox.setObjectName("uiButtonBox")
self.gridLayout.addWidget(self.uiButtonBox, 2, 0, 1, 2)
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.gridLayout.addItem(spacerItem, 3, 0, 1, 1)
self.retranslateUi(PasswordDialog)
self.uiButtonBox.accepted.connect(PasswordDialog.accept) # type: ignore
self.uiButtonBox.rejected.connect(PasswordDialog.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(PasswordDialog)
PasswordDialog.setTabOrder(self.uiPasswordLineEdit, self.uiConfirmPasswordLineEdit)
def retranslateUi(self, PasswordDialog):
_translate = QtCore.QCoreApplication.translate
PasswordDialog.setWindowTitle(_translate("PasswordDialog", "Change password"))
self.uiPasswordLabel.setText(_translate("PasswordDialog", "New password:"))
self.uiConfirmPasswordLabel.setText(_translate("PasswordDialog", "Confirm password:"))

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>603</width>
<height>287</height>
<width>644</width>
<height>290</height>
</rect>
</property>
<property name="windowTitle">
@@ -159,27 +159,37 @@
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="uiRemoteMainServerProtocolLabel">
<property name="text">
<string>Protocol:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="uiRemoteMainServerProtocolComboBox">
<item>
<property name="text">
<string>HTTP</string>
</property>
</item>
<item>
<property name="text">
<string>HTTPS</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Host:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="uiRemoteMainServerPasswordLineEdit">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
<item row="1" column="1">
<widget class="QLineEdit" name="uiRemoteMainServerHostLineEdit"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Port:</string>
@@ -187,9 +197,6 @@
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="uiRemoteMainServerUserLineEdit"/>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="uiRemoteMainServerPortSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@@ -208,16 +215,30 @@
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="uiRemoteMainServerHostLineEdit"/>
</item>
<item row="2" column="0">
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Username:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="uiRemoteMainServerUserLineEdit"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="uiRemoteMainServerPasswordLineEdit">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWizardPage" name="uiSummaryWizardPage">

View File

@@ -2,9 +2,10 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/setup_wizard.ui'
#
# Created by: PyQt5 UI code generator 5.14.1
# Created by: PyQt5 UI code generator 5.15.6
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
@@ -13,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_SetupWizard(object):
def setupUi(self, SetupWizard):
SetupWizard.setObjectName("SetupWizard")
SetupWizard.resize(603, 287)
SetupWizard.resize(644, 290)
SetupWizard.setModal(True)
SetupWizard.setWizardStyle(QtWidgets.QWizard.ModernStyle)
SetupWizard.setOptions(QtWidgets.QWizard.NoBackButtonOnStartPage)
@@ -81,22 +82,23 @@ class Ui_SetupWizard(object):
self.uiRemoteControllerWizardPage.setObjectName("uiRemoteControllerWizardPage")
self.gridLayout = QtWidgets.QGridLayout(self.uiRemoteControllerWizardPage)
self.gridLayout.setObjectName("gridLayout")
self.uiRemoteMainServerProtocolLabel = QtWidgets.QLabel(self.uiRemoteControllerWizardPage)
self.uiRemoteMainServerProtocolLabel.setObjectName("uiRemoteMainServerProtocolLabel")
self.gridLayout.addWidget(self.uiRemoteMainServerProtocolLabel, 0, 0, 1, 1)
self.uiRemoteMainServerProtocolComboBox = QtWidgets.QComboBox(self.uiRemoteControllerWizardPage)
self.uiRemoteMainServerProtocolComboBox.setObjectName("uiRemoteMainServerProtocolComboBox")
self.uiRemoteMainServerProtocolComboBox.addItem("")
self.uiRemoteMainServerProtocolComboBox.addItem("")
self.gridLayout.addWidget(self.uiRemoteMainServerProtocolComboBox, 0, 1, 1, 1)
self.label_3 = QtWidgets.QLabel(self.uiRemoteControllerWizardPage)
self.label_3.setObjectName("label_3")
self.gridLayout.addWidget(self.label_3, 0, 0, 1, 1)
self.uiRemoteMainServerPasswordLineEdit = QtWidgets.QLineEdit(self.uiRemoteControllerWizardPage)
self.uiRemoteMainServerPasswordLineEdit.setEchoMode(QtWidgets.QLineEdit.Password)
self.uiRemoteMainServerPasswordLineEdit.setObjectName("uiRemoteMainServerPasswordLineEdit")
self.gridLayout.addWidget(self.uiRemoteMainServerPasswordLineEdit, 3, 1, 1, 1)
self.label_6 = QtWidgets.QLabel(self.uiRemoteControllerWizardPage)
self.label_6.setObjectName("label_6")
self.gridLayout.addWidget(self.label_6, 3, 0, 1, 1)
self.gridLayout.addWidget(self.label_3, 1, 0, 1, 1)
self.uiRemoteMainServerHostLineEdit = QtWidgets.QLineEdit(self.uiRemoteControllerWizardPage)
self.uiRemoteMainServerHostLineEdit.setObjectName("uiRemoteMainServerHostLineEdit")
self.gridLayout.addWidget(self.uiRemoteMainServerHostLineEdit, 1, 1, 1, 1)
self.label_4 = QtWidgets.QLabel(self.uiRemoteControllerWizardPage)
self.label_4.setObjectName("label_4")
self.gridLayout.addWidget(self.label_4, 1, 0, 1, 1)
self.uiRemoteMainServerUserLineEdit = QtWidgets.QLineEdit(self.uiRemoteControllerWizardPage)
self.uiRemoteMainServerUserLineEdit.setObjectName("uiRemoteMainServerUserLineEdit")
self.gridLayout.addWidget(self.uiRemoteMainServerUserLineEdit, 2, 1, 1, 1)
self.gridLayout.addWidget(self.label_4, 2, 0, 1, 1)
self.uiRemoteMainServerPortSpinBox = QtWidgets.QSpinBox(self.uiRemoteControllerWizardPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
@@ -106,13 +108,20 @@ class Ui_SetupWizard(object):
self.uiRemoteMainServerPortSpinBox.setMaximum(65535)
self.uiRemoteMainServerPortSpinBox.setProperty("value", 3080)
self.uiRemoteMainServerPortSpinBox.setObjectName("uiRemoteMainServerPortSpinBox")
self.gridLayout.addWidget(self.uiRemoteMainServerPortSpinBox, 1, 1, 1, 1)
self.uiRemoteMainServerHostLineEdit = QtWidgets.QLineEdit(self.uiRemoteControllerWizardPage)
self.uiRemoteMainServerHostLineEdit.setObjectName("uiRemoteMainServerHostLineEdit")
self.gridLayout.addWidget(self.uiRemoteMainServerHostLineEdit, 0, 1, 1, 1)
self.gridLayout.addWidget(self.uiRemoteMainServerPortSpinBox, 2, 1, 1, 1)
self.label_5 = QtWidgets.QLabel(self.uiRemoteControllerWizardPage)
self.label_5.setObjectName("label_5")
self.gridLayout.addWidget(self.label_5, 2, 0, 1, 1)
self.gridLayout.addWidget(self.label_5, 3, 0, 1, 1)
self.uiRemoteMainServerUserLineEdit = QtWidgets.QLineEdit(self.uiRemoteControllerWizardPage)
self.uiRemoteMainServerUserLineEdit.setObjectName("uiRemoteMainServerUserLineEdit")
self.gridLayout.addWidget(self.uiRemoteMainServerUserLineEdit, 3, 1, 1, 1)
self.label_6 = QtWidgets.QLabel(self.uiRemoteControllerWizardPage)
self.label_6.setObjectName("label_6")
self.gridLayout.addWidget(self.label_6, 4, 0, 1, 1)
self.uiRemoteMainServerPasswordLineEdit = QtWidgets.QLineEdit(self.uiRemoteControllerWizardPage)
self.uiRemoteMainServerPasswordLineEdit.setEchoMode(QtWidgets.QLineEdit.Password)
self.uiRemoteMainServerPasswordLineEdit.setObjectName("uiRemoteMainServerPasswordLineEdit")
self.gridLayout.addWidget(self.uiRemoteMainServerPasswordLineEdit, 4, 1, 1, 1)
SetupWizard.addPage(self.uiRemoteControllerWizardPage)
self.uiSummaryWizardPage = QtWidgets.QWizardPage()
self.uiSummaryWizardPage.setObjectName("uiSummaryWizardPage")
@@ -149,11 +158,14 @@ class Ui_SetupWizard(object):
self.uiLocalServerPortLabel.setText(_translate("SetupWizard", "Port:"))
self.uiRemoteControllerWizardPage.setTitle(_translate("SetupWizard", "Remote controller"))
self.uiRemoteControllerWizardPage.setSubTitle(_translate("SetupWizard", "Please configure the settings to connect to a remote GNS3 controller"))
self.uiRemoteMainServerProtocolLabel.setText(_translate("SetupWizard", "Protocol:"))
self.uiRemoteMainServerProtocolComboBox.setItemText(0, _translate("SetupWizard", "HTTP"))
self.uiRemoteMainServerProtocolComboBox.setItemText(1, _translate("SetupWizard", "HTTPS"))
self.label_3.setText(_translate("SetupWizard", "Host:"))
self.label_6.setText(_translate("SetupWizard", "Password:"))
self.label_4.setText(_translate("SetupWizard", "Port:"))
self.uiRemoteMainServerPortSpinBox.setSuffix(_translate("SetupWizard", " TCP"))
self.label_5.setText(_translate("SetupWizard", "Username:"))
self.label_6.setText(_translate("SetupWizard", "Password:"))
self.uiSummaryWizardPage.setTitle(_translate("SetupWizard", "Summary"))
self.uiSummaryWizardPage.setSubTitle(_translate("SetupWizard", "The controller has been configured, please see the summary of the settings below"))
self.uiSummaryTreeWidget.headerItem().setText(0, _translate("SetupWizard", "1"))

128
gns3/ui/user_preferences_page.ui Executable file
View File

@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>UserPreferencesPageWidget</class>
<widget class="QWidget" name="UserPreferencesPageWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>472</width>
<height>370</height>
</rect>
</property>
<property name="windowTitle">
<string>User</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="uiSettingsGroupBox">
<property name="title">
<string>Logged-in user information</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0">
<widget class="QLabel" name="uiEmailLabel">
<property name="text">
<string>Email address:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="uiFullNameLabel">
<property name="text">
<string>Full name:</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="uiUsernameLabel">
<property name="text">
<string>Username:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="uiUsernameLineEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="uiFullNameLineEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="uiEmailLineEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="uiChangePasswordPushButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&amp;Change password</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiCopyAccessTokenPushButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&amp;Copy access token</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/user_preferences_page.ui'
#
# Created by: PyQt5 UI code generator 5.15.10
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_UserPreferencesPageWidget(object):
def setupUi(self, UserPreferencesPageWidget):
UserPreferencesPageWidget.setObjectName("UserPreferencesPageWidget")
UserPreferencesPageWidget.resize(472, 370)
self.verticalLayout = QtWidgets.QVBoxLayout(UserPreferencesPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiSettingsGroupBox = QtWidgets.QGroupBox(UserPreferencesPageWidget)
self.uiSettingsGroupBox.setObjectName("uiSettingsGroupBox")
self.gridLayout = QtWidgets.QGridLayout(self.uiSettingsGroupBox)
self.gridLayout.setObjectName("gridLayout")
self.uiEmailLabel = QtWidgets.QLabel(self.uiSettingsGroupBox)
self.uiEmailLabel.setObjectName("uiEmailLabel")
self.gridLayout.addWidget(self.uiEmailLabel, 3, 0, 1, 1)
self.uiFullNameLabel = QtWidgets.QLabel(self.uiSettingsGroupBox)
self.uiFullNameLabel.setObjectName("uiFullNameLabel")
self.gridLayout.addWidget(self.uiFullNameLabel, 2, 0, 1, 1)
self.uiUsernameLabel = QtWidgets.QLabel(self.uiSettingsGroupBox)
self.uiUsernameLabel.setObjectName("uiUsernameLabel")
self.gridLayout.addWidget(self.uiUsernameLabel, 0, 0, 1, 1)
self.uiUsernameLineEdit = QtWidgets.QLineEdit(self.uiSettingsGroupBox)
self.uiUsernameLineEdit.setReadOnly(True)
self.uiUsernameLineEdit.setObjectName("uiUsernameLineEdit")
self.gridLayout.addWidget(self.uiUsernameLineEdit, 0, 1, 1, 1)
self.uiFullNameLineEdit = QtWidgets.QLineEdit(self.uiSettingsGroupBox)
self.uiFullNameLineEdit.setReadOnly(True)
self.uiFullNameLineEdit.setObjectName("uiFullNameLineEdit")
self.gridLayout.addWidget(self.uiFullNameLineEdit, 2, 1, 1, 1)
self.uiEmailLineEdit = QtWidgets.QLineEdit(self.uiSettingsGroupBox)
self.uiEmailLineEdit.setReadOnly(True)
self.uiEmailLineEdit.setObjectName("uiEmailLineEdit")
self.gridLayout.addWidget(self.uiEmailLineEdit, 3, 1, 1, 1)
self.verticalLayout.addWidget(self.uiSettingsGroupBox)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.uiChangePasswordPushButton = QtWidgets.QPushButton(UserPreferencesPageWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiChangePasswordPushButton.sizePolicy().hasHeightForWidth())
self.uiChangePasswordPushButton.setSizePolicy(sizePolicy)
self.uiChangePasswordPushButton.setObjectName("uiChangePasswordPushButton")
self.horizontalLayout.addWidget(self.uiChangePasswordPushButton)
self.uiCopyAccessTokenPushButton = QtWidgets.QPushButton(UserPreferencesPageWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.uiCopyAccessTokenPushButton.sizePolicy().hasHeightForWidth())
self.uiCopyAccessTokenPushButton.setSizePolicy(sizePolicy)
self.uiCopyAccessTokenPushButton.setObjectName("uiCopyAccessTokenPushButton")
self.horizontalLayout.addWidget(self.uiCopyAccessTokenPushButton)
self.verticalLayout.addLayout(self.horizontalLayout)
spacerItem1 = QtWidgets.QSpacerItem(20, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem1)
self.retranslateUi(UserPreferencesPageWidget)
QtCore.QMetaObject.connectSlotsByName(UserPreferencesPageWidget)
def retranslateUi(self, UserPreferencesPageWidget):
_translate = QtCore.QCoreApplication.translate
UserPreferencesPageWidget.setWindowTitle(_translate("UserPreferencesPageWidget", "User"))
self.uiSettingsGroupBox.setTitle(_translate("UserPreferencesPageWidget", "Logged-in user information"))
self.uiEmailLabel.setText(_translate("UserPreferencesPageWidget", "Email address:"))
self.uiFullNameLabel.setText(_translate("UserPreferencesPageWidget", "Full name:"))
self.uiUsernameLabel.setText(_translate("UserPreferencesPageWidget", "Username:"))
self.uiChangePasswordPushButton.setText(_translate("UserPreferencesPageWidget", "&Change password"))
self.uiCopyAccessTokenPushButton.setText(_translate("UserPreferencesPageWidget", "&Copy access token"))

View File

@@ -23,8 +23,8 @@
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "3.0.2"
__version_info__ = (3, 0, 2, 0)
__version__ = "3.0.4"
__version_info__ = (3, 0, 4, 0)
if "dev" in __version__:
try:

View File

@@ -1,6 +1,6 @@
jsonschema>=4.23,<4.24
sentry-sdk>=2.19.2,<2.20 # optional dependency
psutil>=6.1.1
sentry-sdk>=2.22,<2.23 # optional dependency
psutil>=7.0.0
distro>=1.9.0
truststore>=0.10.0; python_version >= '3.10'
truststore>=0.10.1; python_version >= '3.10'
importlib-resources>=1.3; python_version < '3.9'

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
enable-background="new -0.709 -32.081 141.732 141.732"
height="141.732px"
id="Livello_1"
version="1.1"
viewBox="-0.709 -32.081 141.732 141.732"
width="141.732px"
xml:space="preserve"
sodipodi:docname="eye_off2.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs9" /><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="5.0729548"
inkscape:cx="71.161683"
inkscape:cy="70.865997"
inkscape:window-width="1658"
inkscape:window-height="1016"
inkscape:window-x="70"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="Livello_1" /><g
id="Livello_80"><path
d="M89.668,38.786c0-10.773-8.731-19.512-19.51-19.512S50.646,28.01,50.646,38.786c0,10.774,8.732,19.511,19.512,19.511 C80.934,58.297,89.668,49.561,89.668,38.786 M128.352,38.727c-13.315,17.599-34.426,28.972-58.193,28.972 c-23.77,0-44.879-11.373-58.194-28.972C25.279,21.129,46.389,9.756,70.158,9.756C93.927,9.756,115.036,21.129,128.352,38.727 M140.314,38.76C125.666,15.478,99.725,0,70.158,0S14.648,15.478,0,38.76c14.648,23.312,40.591,38.81,70.158,38.81 S125.666,62.072,140.314,38.76"
id="path2" /></g><g
id="Livello_1_1_" /><path
d="m 19.354144,-16.791815 c 2.079678,-2.079624 5.451424,-2.079624 7.5311,0 L 122.74,79.063042 c 2.07952,2.079517 2.07952,5.451476 0,7.530992 -2.07952,2.079517 -5.45148,2.079517 -7.53099,0 L 19.354144,-9.2607185 c -2.079623,-2.0796765 -2.079623,-5.4514205 0,-7.5310965 z"
id="path905"
style="fill:#ffffff;stroke-width:5.32526" /><path
d="m 13.899155,-13.386617 c 2.079678,-2.079624 5.451424,-2.079624 7.5311,0 L 117.28501,82.46824 c 2.07952,2.079517 2.07952,5.451476 0,7.530992 -2.07952,2.079517 -5.45148,2.079517 -7.53099,0 L 13.899155,-5.8555208 c -2.079623,-2.0796767 -2.079623,-5.4514202 0,-7.5310962 z"
id="path2-3"
style="stroke-width:5.32526" /></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new -0.709 -32.081 141.732 141.732" height="141.732px" id="Livello_1" version="1.1" viewBox="-0.709 -32.081 141.732 141.732" width="141.732px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Livello_80"><path d="M89.668,38.786c0-10.773-8.731-19.512-19.51-19.512S50.646,28.01,50.646,38.786c0,10.774,8.732,19.511,19.512,19.511 C80.934,58.297,89.668,49.561,89.668,38.786 M128.352,38.727c-13.315,17.599-34.426,28.972-58.193,28.972 c-23.77,0-44.879-11.373-58.194-28.972C25.279,21.129,46.389,9.756,70.158,9.756C93.927,9.756,115.036,21.129,128.352,38.727 M140.314,38.76C125.666,15.478,99.725,0,70.158,0S14.648,15.478,0,38.76c14.648,23.312,40.591,38.81,70.158,38.81 S125.666,62.072,140.314,38.76"/></g><g id="Livello_1_1_"/></svg>

After

Width:  |  Height:  |  Size: 935 B

View File

@@ -99,6 +99,8 @@
<file>icons/camera-photo-hover.svg</file>
<file>icons/front.svg</file>
<file>icons/dialog-warning.svg</file>
<file>icons/eye-on.svg</file>
<file>icons/eye-off.svg</file>
<file>images/gns3_logo.png</file>
<file>images/gns3_icon_128x128.png</file>
<file>images/gns3_icon_256x256.png</file>