Compare commits

...

19 Commits

Author SHA1 Message Date
grossmj
9dd4020666 Release v3.0.2 2025-01-03 21:44:01 +07:00
grossmj
83d0957d50 Add button to create templates based on images that are not used by any yet 2025-01-03 21:34:17 +07:00
grossmj
1767a441ab Add "prune" images button in image management dialog. 2025-01-02 22:53:36 +07:00
grossmj
41b7c2c33e Use the controller image endpoint to install appliances 2024-12-30 22:10:36 +07:00
Jeremy Grossmann
1cebcabff3 Merge pull request #3688 from GNS3/drop-python3.8
Drop Python 3.8
2024-12-30 15:39:19 +07:00
grossmj
53cc823859 Drop Python 3.8 2024-12-30 15:36:45 +07:00
grossmj
dd6329a1f3 Add image info tooltip in image management dialog. 2024-12-30 12:00:46 +07:00
grossmj
68633c4732 Upgrade dependencies 2024-12-28 18:21:22 +07:00
grossmj
161421abcf Merge branch '2.2' into 3.0 2024-12-28 18:04:18 +07:00
grossmj
9466b2a1fb Merge branch 'master' into 2.2 2024-12-28 18:03:26 +07:00
grossmj
889d41636d Release v3.0.1 2024-12-27 21:05:25 +07:00
grossmj
878cfb2fe5 Fix issue when image is already on the local server. Fixes https://github.com/GNS3/gns3-gui/issues/3678 2024-12-26 22:05:36 +07:00
grossmj
141767e0d9 Fix image uploading when image name differs with the image name recorded in the appliance. Fixes https://github.com/GNS3/gns3-gui/issues/3682 2024-12-26 19:33:00 +07:00
grossmj
a5976a61ac Merge branch '2.2' into 3.0 2024-12-25 22:17:08 +07:00
grossmj
3edde1274b Fix Linux Mint default terminal configuration 2024-12-25 22:14:25 +07:00
Jeremy Grossmann
e17e7fc033 Merge pull request #3672 from ob7/enable-css-grid-colors
Add CSS Grid Color Customization Support
2024-12-02 19:56:29 +10:00
ob7
bfe11d7976 apply grid color via css property 2024-12-02 00:13:01 -09:00
Jeremy Grossmann
2b98d51ff7 Merge pull request #3668 from GNS3/release/v2.2.52
release/v2.2.52
2024-12-02 11:35:42 +10:00
Jeremy Grossmann
5efb3019f4 Merge pull request #3656 from GNS3/release/v2.2.51
release/v2.2.51
2024-11-07 23:13:11 +10:00
19 changed files with 226 additions and 83 deletions

View File

@@ -1,5 +1,21 @@
# Change Log
## 3.0.2 03/01/2025
* Add button to create templates based on images that are not used by any yet.
* Add "prune" images button in image management dialog.
* Use the controller image endpoint to install appliances
* Drop Python 3.8
* Add image info tooltip in image management dialog.
* Upgrade dependencies
* Apply grid color via css property
## 3.0.1 27/12/2024
* Fix issue when image is already on the local server. Fixes https://github.com/GNS3/gns3-gui/issues/3678
* Fix image uploading when image name differs with the image name recorded in the appliance. Fixes https://github.com/GNS3/gns3-gui/issues/3682
* Fix Linux Mint default terminal configuration
## 3.0.0 20/12/2024
* Change title of QMessageBox

View File

@@ -237,7 +237,7 @@ class Controller(QtCore.QObject):
def request(self, method, path, *args, **kwargs):
"""
Forward the query to the HTTP client or controller depending of the path
Forward the query to the HTTP client or controller depending on the path
"""
if self._http_client:

View File

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

View File

@@ -174,7 +174,7 @@ class ApplianceWizard(QtWidgets.QWizard, Ui_ApplianceWizard):
elif self.page(page_id) == self.uiFilesWizardPage:
if Controller.instance().isRemote() or self._compute_id != "local":
self._registry.getRemoteImageList(self._appliance.template_type(), self._compute_id)
self._registry.getRemoteImageList()
else:
self.images_changed_signal.emit()
@@ -212,7 +212,7 @@ Usage: {}
log.error("Error while uploading image '{}': {}".format(image_path, result["message"]))
else:
log.info("Image '{}' has been successfully uploaded".format(image_path))
self._registry.getRemoteImageList(self._appliance.template_type(), self._compute_id)
self._registry.getRemoteImageList()
def _showApplianceInfoSlot(self):
"""
@@ -329,7 +329,7 @@ Usage: {}
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("red")))
else:
image_widget.setForeground(2, QtGui.QBrush(QtGui.QColor("green")))
image_widget.setToolTip(2, image["path"])
image_widget.setToolTip(0, f'{image["status"]} with path: {image["path"]}')
# Associated data stored are col 0: version, col 1: image
image_widget.setData(0, QtCore.Qt.UserRole, version)
@@ -383,11 +383,13 @@ Usage: {}
for version in self._appliance["versions"]:
for image in version["images"].values():
img = self._registry.search_image_file(self._appliance.template_type(),
image["filename"],
image.get("md5sum"),
image.get("filesize"),
strict_md5_check=not self.allowCustomFiles.isChecked())
img = self._registry.search_image_file(
self._appliance.template_type(),
image["filename"],
image.get("md5sum"),
image.get("filesize"),
strict_md5_check=not self.allowCustomFiles.isChecked()
)
if img:
if img.location == "local":
image["status"] = "Found locally"
@@ -551,21 +553,6 @@ Usage: {}
TemplateManager.instance().createTemplate(Template(new_template), callback=self._templateCreatedCallback)
return False
#worker = WaitForLambdaWorker(lambda: self._create_template(appliance_configuration, self._compute_id), allowed_exceptions=[ConfigException, OSError])
#progress_dialog = ProgressDialog(worker, "Add template", "Installing a new template...", None, busy=True, parent=self)
#progress_dialog.show()
#if progress_dialog.exec_():
# QtWidgets.QMessageBox.information(self.parent(), "Add template", "{} template has been installed!".format(appliance_configuration["name"]))
# return True
#return False
# worker = WaitForLambdaWorker(lambda: config.save(), allowed_exceptions=[ConfigException, OSError])
# progress_dialog = ProgressDialog(worker, "Add appliance", "Install the appliance...", None, busy=True, parent=self)
# progress_dialog.show()
# if progress_dialog.exec_():
# QtWidgets.QMessageBox.information(self.parent(), "Add appliance", "{} installed!".format(appliance_configuration["name"]))
# return True
def _templateCreatedCallback(self, result, error=False, **kwargs):
if error is True:
@@ -590,7 +577,7 @@ Usage: {}
if image["location"] == "local":
if not Controller.instance().isRemote() and self._compute_id == "local" and image["path"].startswith(ImageManager.instance().getDirectory()):
log.debug("{} is already on the local server".format(image["path"]))
return
return True
image = Image(self._appliance.template_type(), image["path"], filename=image["filename"])
image_upload_manager = ImageUploadManager(image, Controller.instance(), self.parent())
if not image_upload_manager.upload():

View File

@@ -18,7 +18,7 @@
import os
import pathlib
from gns3.http_client_error import HttpClientError, HttpClientCancelledRequestError
from ..qt import QtCore, QtWidgets, qslot, sip_is_deleted
from ..qt import QtCore, QtGui, QtWidgets, qslot, sip_is_deleted
from ..ui.image_dialog_ui import Ui_ImageDialog
from ..utils import human_size
from ..controller import Controller
@@ -41,6 +41,8 @@ class ImageDialog(QtWidgets.QDialog, Ui_ImageDialog):
self.setupUi(self)
self.uiUploadImagePushButton.clicked.connect(self._uploadImageSlot)
self.uiDeleteImagePushButton.clicked.connect(self._deleteImageSlot)
self.uiInstallAllPushButton.clicked.connect(self._installAllSlot)
self.uiPruneImagesPushButton.clicked.connect(self._pruneImagesSlot)
self.uiRefreshImagesPushButton.clicked.connect(Controller.instance().refreshImageList)
Controller.instance().image_list_updated_signal.connect(self._updateImageListSlot)
self._updateImageListSlot()
@@ -136,6 +138,64 @@ class ImageDialog(QtWidgets.QDialog, Ui_ImageDialog):
Controller.instance().refreshImageList()
def _installAllSlot(self, *args):
reply = QtWidgets.QMessageBox.warning(
self,
"Install appliance(s)",
"This will attempt to automatically create templates based on image checksums.\nContinue?",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No
)
if reply == QtWidgets.QMessageBox.No:
return
Controller.instance().post(
f"/images/install",
progress_text=f"Installing appliances",
timeout=None,
wait=True
)
@qslot
def _pruneImagesSlot(self, *args):
reply = QtWidgets.QMessageBox.warning(
self,
"Prune image(s)",
"Delete all images not used by a template?\nThis cannot be reverted.",
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No
)
if reply == QtWidgets.QMessageBox.No:
return
error_msgs = ""
try:
Controller.instance().delete(
f"/images/prune",
progress_text=f"Pruning images",
timeout=None,
wait=True
)
except HttpClientCancelledRequestError:
return
except HttpClientError as e:
error_msgs += f"{e}\n"
if error_msgs:
error_dialog = QtWidgets.QMessageBox(self)
error_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
error_dialog.setWindowTitle("Image pruning")
error_dialog.setText(f"Error while deleting images on the controller")
error_dialog.setDetailedText(error_msgs)
error_dialog.setIcon(QtWidgets.QMessageBox.Critical)
error_dialog.show()
Controller.instance().refreshImageList()
@qslot
def _updateImageListSlot(self, *args):
@@ -146,6 +206,7 @@ class ImageDialog(QtWidgets.QDialog, Ui_ImageDialog):
for image in Controller.instance().images():
item = QtWidgets.QTreeWidgetItem([image["filename"], image["image_type"], human_size(image["image_size"])])
item.setData(0, QtCore.Qt.UserRole, image["filename"])
item.setToolTip(0, f'{image["filename"]} {image["checksum"]}')
items.append(item)
self.uiImagesTreeWidget.addTopLevelItems(items)
@@ -159,6 +220,31 @@ class ImageDialog(QtWidgets.QDialog, Ui_ImageDialog):
self.uiImagesTreeWidget.sortItems(0, QtCore.Qt.AscendingOrder)
self.uiImagesTreeWidget.setUpdatesEnabled(True)
def contextMenuEvent(self, event):
"""
Handles all context menu events.
:param event: QContextMenuEvent instance
"""
items = self.uiImagesTreeWidget.selectedItems()
if items:
menu = QtWidgets.QMenu()
copy = QtWidgets.QAction("&Copy image information to clipboard", menu)
copy.triggered.connect(self._copyToClipboardSlot)
menu.addAction(copy)
menu.exec_(event.globalPos())
def _copyToClipboardSlot(self):
"""
Copies the selected image tooltip to the clipboard.
"""
items = self.uiImagesTreeWidget.selectedItems()
if items:
QtWidgets.QApplication.clipboard().setText(items[0].toolTip(0))
log.info(f"'{items[0].toolTip(0)}' copied to clipboard")
def keyPressEvent(self, e):
"""
Event handler in order to properly handle escape.
@@ -166,3 +252,5 @@ class ImageDialog(QtWidgets.QDialog, Ui_ImageDialog):
if e.key() == QtCore.Qt.Key_Escape:
self.close()
elif e.matches(QtGui.QKeySequence.Copy):
self._copyToClipboardSlot()

View File

@@ -74,6 +74,10 @@ class GraphicsView(QtWidgets.QGraphicsView):
:param parent: parent widget
"""
# Class-level constants for default colors
DEFAULT_DRAWING_GRID_COLOR = QtGui.QColor(208, 208, 208) # #D0D0D0
DEFAULT_NODE_GRID_COLOR = QtGui.QColor(190, 190, 190) # #BEBEBE
def __init__(self, parent):
# Our parent is the central widget which parent is the main window.
@@ -92,6 +96,8 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._dragging = False
self._grid_size = 75
self._drawing_grid_size = 25
self._drawing_grid_color = self.DEFAULT_DRAWING_GRID_COLOR
self._node_grid_color = self.DEFAULT_NODE_GRID_COLOR
self._last_mouse_position = None
self._topology = Topology.instance()
@@ -1685,11 +1691,39 @@ class GraphicsView(QtWidgets.QGraphicsView):
self._topology.addDrawing(item)
return item
@QtCore.Property(QtGui.QColor)
def drawingGridColor(self):
"""Returns the drawing grid color"""
return self._drawing_grid_color
@drawingGridColor.setter
def drawingGridColor(self, color):
"""Sets the drawing grid color"""
self._drawing_grid_color = color
self.viewport().update()
@QtCore.Property(QtGui.QColor)
def nodeGridColor(self):
"""Returns the node grid color"""
return self._node_grid_color
@nodeGridColor.setter
def nodeGridColor(self, color):
"""Sets the node grid color"""
self._node_grid_color = color
self.viewport().update()
def resetGridColors(self):
"""Reset grid colors to defaults"""
self._drawing_grid_color = self.DEFAULT_DRAWING_GRID_COLOR
self._node_grid_color = self.DEFAULT_NODE_GRID_COLOR
self.viewport().update()
def drawBackground(self, painter, rect):
super().drawBackground(painter, rect)
if self._main_window.uiShowGridAction.isChecked():
grids = [(self.drawingGridSize(), QtGui.QColor(208, 208, 208)),
(self.nodeGridSize(), QtGui.QColor(190, 190, 190))]
grids = [(self.drawingGridSize(), self._drawing_grid_color),
(self.nodeGridSize(), self._node_grid_color)]
painter.save()
for (grid, colour) in grids:
if not grid:

View File

@@ -63,7 +63,7 @@ class ImageUploadManager(object):
except HttpClientError as e:
QtWidgets.QMessageBox.critical(
self._parent,
"Image upload",
"Image upload to controller",
f"Could not upload image {self._image.filename}: {e}"
)
return False

View File

@@ -189,9 +189,9 @@ def main():
# catch exceptions to write them in a file
sys.excepthook = exceptionHook
# we only support Python 3 version >= 3.8
if sys.version_info < (3, 8):
raise SystemExit("Python 3.8 or higher is required")
# we only support Python 3 version >= 3.9
if sys.version_info < (3, 9):
raise SystemExit("Python 3.9 or higher is required")
if parse_version(QtCore.QT_VERSION_STR) < parse_version("5.5.0"):
raise SystemExit("Requirement is PyQt5 version 5.5.0 or higher, got version {}".format(QtCore.QT_VERSION_STR))

View File

@@ -232,7 +232,7 @@ class NodesView(QtWidgets.QTreeWidget):
msgbox.setText(f"Do you want to delete template '{template.name()}'?\n\n"
f"Deleting templates and images is irreversible!\n\n")
msgbox.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
delete_all_button = QtWidgets.QPushButton(f"&Delete the template and orphaned images", msgbox)
delete_all_button = QtWidgets.QPushButton(f"&Delete the template and its image(s)", msgbox)
msgbox.addButton(delete_all_button, QtWidgets.QMessageBox.YesRole)
delete_template_only_button = QtWidgets.QPushButton(f"&Only delete the template", msgbox)
msgbox.addButton(delete_template_only_button, QtWidgets.QMessageBox.NoRole)

View File

@@ -112,12 +112,11 @@ class LogQMessageBox(QtWidgets.QMessageBox):
Return a logger in the context of the caller
in order to have the correct information in the log
"""
if sys.version_info < (3, 5):
return logging.getLogger('qt')
try:
caller = inspect.stack()[2]
location = "{}:{}".format(os.path.basename(caller.filename), caller.lineno)
except:
except Exception:
# If anything go wrong during the format return the standard logger
# for unknonw reason sometimes we don't have the caller info
return logging.getLogger('qt')

View File

@@ -156,7 +156,7 @@ class ApplianceToTemplate:
for image in appliance_config["images"]:
if image.get("path"):
new_config[image["type"]] = self._relative_image_path("QEMU", image["path"])
new_config[image["type"]] = image["filename"]
if "arch" in appliance_config:
new_config["platform"] = appliance_config["arch"]
@@ -188,38 +188,21 @@ class ApplianceToTemplate:
new_config["template_type"] = "dynamips"
new_config.update(template_properties)
for image in appliance_config["images"]:
new_config[image["type"]] = self._relative_image_path("IOS", image["path"])
new_config[image["type"]] = image["filename"]
if self._registry_version < 8:
new_config["idlepc"] = image.get("idlepc", "")
if "image" not in new_config:
raise ConfigException("Disk image is missing")
def _add_iou_config(self, new_config, template_properties, appliance_config):
new_config["template_type"] = "iou"
new_config.update(template_properties)
for image in appliance_config["images"]:
if "path" not in image:
raise ConfigException("Disk image is missing")
new_config[image["type"]] = self._relative_image_path("IOU", image["path"])
new_config["path"] = new_config["image"]
def _relative_image_path(self, image_dir_type, path):
"""
:param image_dir_type: Type of image directory
:param filename: Filename at the end of the process
:param path: Full path to the file
:returns: Path relative to image directory.
Copy the image to the directory if not already in the directory
"""
images_dir = os.path.join(Config().images_dir, image_dir_type)
path = os.path.abspath(path)
if os.path.commonprefix([images_dir, path]) == images_dir:
return path.replace(images_dir, '').strip('/\\')
return os.path.basename(path)
new_config["path"] = image["filename"]
if "path" not in new_config:
raise ConfigException("Disk image is missing")
def _set_symbol(self, symbol_id, controller_symbols):
"""

View File

@@ -47,23 +47,25 @@ class Registry(QtCore.QObject):
:param image_directory: Folder we need to add
"""
self._images_dirs.append(image_directory)
def getRemoteImageList(self, emulator, compute_id):
self._emulator = emulator
Controller.instance().getCompute("/{}/images".format(emulator), compute_id, self._getRemoteListCallback, progress_text="Listing remote images...")
def getRemoteImageList(self):
Controller.instance().get("/images", self._getRemoteListCallback, progress_text="Listing remote images...")
def _getRemoteListCallback(self, result, error=False, **kwargs):
if error:
if "message" in result:
log.error("Error while getting the list of remote images: {}".format(result["message"]))
return
self._remote_images = []
for res in result:
image = Image(self._emulator, res["path"])
image = Image(res["image_type"], res["path"])
image.location = "remote"
image.md5sum = res.get("md5sum")
image.filesize = res.get("filesize")
image.md5sum = res.get("checksum")
image.filesize = res.get("image_size")
self._remote_images.append(image)
self.image_list_changed_signal.emit()

View File

@@ -168,7 +168,7 @@ else:
if sys.platform.startswith("linux"):
distro_name = distro.name()
if distro_name == "Debian" or distro_name == "Ubuntu" or distro_name == "LinuxMint":
if distro_name == "Debian" or distro_name == "Ubuntu" or distro_name == "Linux Mint":
if shutil.which("mate-terminal"):
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Mate Terminal"]
else:

View File

@@ -47,6 +47,10 @@ class Style:
Sets the legacy GUI style.
"""
graphics_view = self._mw.uiGraphicsView
if hasattr(graphics_view, 'resetGridColors'):
graphics_view.resetGridColors()
self._mw.setStyleSheet("")
self._mw.uiNewProjectAction.setIcon(QtGui.QIcon(":/icons/new-project.svg"))
self._mw.uiOpenProjectAction.setIcon(QtGui.QIcon(":/icons/open.svg"))
@@ -100,6 +104,10 @@ class Style:
Sets the classic GUI style.
"""
graphics_view = self._mw.uiGraphicsView
if hasattr(graphics_view, 'resetGridColors'):
graphics_view.resetGridColors()
self._mw.setStyleSheet("")
self._mw.uiNewProjectAction.setIcon(self._getStyleIcon(":/classic_icons/new-project.svg", ":/classic_icons/new-project-hover.svg"))
self._mw.uiOpenProjectAction.setIcon(self._getStyleIcon(":/classic_icons/open.svg", ":/classic_icons/open-hover.svg"))
@@ -157,6 +165,10 @@ class Style:
Sets the charcoal GUI style.
"""
graphics_view = self._mw.uiGraphicsView
if hasattr(graphics_view, 'resetGridColors'):
graphics_view.resetGridColors()
style_file = QtCore.QFile(":/styles/charcoal.css")
style_file.open(QtCore.QFile.ReadOnly)
style = QtCore.QTextStream(style_file).readAll()

View File

@@ -9,8 +9,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>732</width>
<height>329</height>
<width>849</width>
<height>328</height>
</rect>
</property>
<property name="windowTitle">
@@ -26,7 +26,7 @@
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="sortingEnabled">
<bool>true</bool>
@@ -90,6 +90,20 @@
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="uiInstallAllPushButton">
<property name="text">
<string>&amp;Install all</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiPruneImagesPushButton">
<property name="text">
<string>&amp;Prune</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uiRefreshImagesPushButton">
<property name="text">

View File

@@ -2,9 +2,10 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/image_dialog.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
@@ -14,13 +15,13 @@ class Ui_ImageDialog(object):
def setupUi(self, ImageDialog):
ImageDialog.setObjectName("ImageDialog")
ImageDialog.setWindowModality(QtCore.Qt.ApplicationModal)
ImageDialog.resize(732, 329)
ImageDialog.resize(849, 328)
ImageDialog.setModal(True)
self.gridLayout = QtWidgets.QGridLayout(ImageDialog)
self.gridLayout.setObjectName("gridLayout")
self.uiImagesTreeWidget = QtWidgets.QTreeWidget(ImageDialog)
self.uiImagesTreeWidget.setAlternatingRowColors(True)
self.uiImagesTreeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.uiImagesTreeWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
self.uiImagesTreeWidget.setObjectName("uiImagesTreeWidget")
self.uiImagesTreeWidget.header().setSortIndicatorShown(True)
self.gridLayout.addWidget(self.uiImagesTreeWidget, 0, 0, 1, 1)
@@ -38,6 +39,12 @@ class Ui_ImageDialog(object):
self.horizontalLayout.addWidget(self.uiInstallApplianceCheckBox)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.uiInstallAllPushButton = QtWidgets.QPushButton(ImageDialog)
self.uiInstallAllPushButton.setObjectName("uiInstallAllPushButton")
self.horizontalLayout.addWidget(self.uiInstallAllPushButton)
self.uiPruneImagesPushButton = QtWidgets.QPushButton(ImageDialog)
self.uiPruneImagesPushButton.setObjectName("uiPruneImagesPushButton")
self.horizontalLayout.addWidget(self.uiPruneImagesPushButton)
self.uiRefreshImagesPushButton = QtWidgets.QPushButton(ImageDialog)
self.uiRefreshImagesPushButton.setObjectName("uiRefreshImagesPushButton")
self.horizontalLayout.addWidget(self.uiRefreshImagesPushButton)
@@ -49,8 +56,8 @@ class Ui_ImageDialog(object):
self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 1)
self.retranslateUi(ImageDialog)
self.uiButtonBox.accepted.connect(ImageDialog.accept)
self.uiButtonBox.rejected.connect(ImageDialog.reject)
self.uiButtonBox.accepted.connect(ImageDialog.accept) # type: ignore
self.uiButtonBox.rejected.connect(ImageDialog.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(ImageDialog)
def retranslateUi(self, ImageDialog):
@@ -63,4 +70,6 @@ class Ui_ImageDialog(object):
self.uiUploadImagePushButton.setText(_translate("ImageDialog", "&Upload"))
self.uiDeleteImagePushButton.setText(_translate("ImageDialog", "&Delete"))
self.uiInstallApplianceCheckBox.setText(_translate("ImageDialog", "&Install appliance(s) after upload"))
self.uiInstallAllPushButton.setText(_translate("ImageDialog", "&Install all"))
self.uiPruneImagesPushButton.setText(_translate("ImageDialog", "&Prune"))
self.uiRefreshImagesPushButton.setText(_translate("ImageDialog", "&Refresh"))

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.0"
__version_info__ = (3, 0, 0, 0)
__version__ = "3.0.2"
__version_info__ = (3, 0, 2, 0)
if "dev" in __version__:
try:

View File

@@ -10,7 +10,7 @@ authors = [
{ name = "Jeremy Grossmann", email = "developers@gns3.com" }
]
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.9"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: X11 Applications :: Qt",
@@ -22,7 +22,6 @@ classifiers = [
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",

View File

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