Merge pull request #1814 from GNS3/show_error

Display an overlay popup with log messages
This commit is contained in:
Julien Duponchelle
2017-02-17 11:20:37 +01:00
committed by GitHub
10 changed files with 233 additions and 10 deletions

View File

@@ -21,7 +21,6 @@ Handles commands typed in the GNS3 console.
import sys
import cmd
import logging
import struct
import sip
import json
@@ -30,6 +29,9 @@ from .node import Node
from .qt import QtCore
from .version import __version__
import logging
log = logging.getLogger(__name__)
class ConsoleCmd(cmd.Cmd):
@@ -177,6 +179,24 @@ class ConsoleCmd(cmd.Cmd):
print("Cannot console to {}".format(device))
break
def do_log(self, args):
"""
Log a message
log level message
"""
args = args.split()
if len(args) == 0:
return
level = args.pop(0)
if level == "info":
log.info(" ".join(args))
elif level == "warning":
log.warning(" ".join(args))
else:
log.error(" ".join(args))
def _start_console(self, node):
"""
Starts a console application for a specific node.

View File

@@ -0,0 +1,186 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 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/>.
"""
Display error to the user in an overlay popup
"""
import os
import time
from gns3.qt import QtWidgets, QtCore, qslot
import logging
log = logging.getLogger(__name__)
MAX_ELEMENTS = 3
DISPLAY_DURATION = {
"CRITICAL": 120,
"ERROR": 120,
"WARNING": 20,
"INFO": 5
}
class NotifDialogHandler(logging.StreamHandler):
def __init__(self, dialog):
super().__init__()
self._dialog = dialog
self.setLevel(logging.INFO)
self._dialog.show()
def emit(self, record):
self._dialog.addNotif(record.levelname, record.getMessage())
class NotifDialog(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
self._notifs = []
self.setWindowFlags(QtCore.Qt.FramelessWindowHint |
QtCore.Qt.WindowDoesNotAcceptFocus |
QtCore.Qt.SubWindow)
# QtCore.Qt.Tool)
# QtCore.Qt.WindowStaysOnTopHint)
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) # | QtCore.Qt.WA_TranslucentBackground)
self._layout = QtWidgets.QVBoxLayout()
self._timer = QtCore.QTimer()
self._timer.setInterval(1000)
self._timer.timeout.connect(self._refreshSlot)
self._timer.start()
for i in range(0, MAX_ELEMENTS):
l = QtWidgets.QLabel()
l.setAlignment(QtCore.Qt.AlignTop)
l.setWordWrap(True)
l.hide()
self._layout.addWidget(l)
self.setLayout(self._layout)
@qslot
def addNotif(self, level, message):
if not self.parent().settings()["overlay_notifications"]:
return
# This unicode char prevent the wordwrap at /
message = message.replace("/", "\u2060/\u2060")
if len(self._notifs) == MAX_ELEMENTS:
self._notifs.pop(0)
self._notifs.append((level, message, time.time()))
self.update()
@qslot
def _refreshSlot(self):
"""
Hide the notifs after some delay
"""
notifs = []
for (i, (level, message, when)) in enumerate(self._notifs):
if when + DISPLAY_DURATION[level] > time.time():
notifs.append((level, message, when))
if notifs != self._notifs:
self._notifs = notifs
self.update()
def update(self):
if len(self._notifs) == 0:
self.hide()
else:
for (i, (level, message, when)) in enumerate(self._notifs):
w = self._layout.itemAt(i).widget()
w.setText(message)
if level == "ERROR" or level == "CRITICAL":
w.setStyleSheet("""
color: black;
padding-left: 12px;
background-color: rgb(247, 205, 198);
border-left: 10px solid red;
""")
elif level == "WARNING":
w.setStyleSheet("""
color: black;
padding-left: 12px;
background-color: #f4f2b5;
border-left: 10px solid orange;
""")
elif level == "INFO":
w.setStyleSheet("""
color: black;
padding-left: 12px;
background-color: #cfffc9;
border-left: 10px solid green;
""")
w.show()
for i in range(i + 1, MAX_ELEMENTS):
w = self._layout.itemAt(i).widget()
w.hide()
x = self.parent().width() - self.width() - 10
y = 10
self.setGeometry(x, y, self.sizeHint().width(), self.sizeHint().height())
self.show()
@qslot
def mousePressEvent(self, event):
self._notifs.clear()
self.update()
if __name__ == '__main__':
"""
A demo main for testing the features
"""
import sys
app = QtWidgets.QApplication(sys.argv)
logging.basicConfig(level=logging.INFO)
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
l1 = QtWidgets.QLabel()
l1.setText("Hello World")
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(l1)
self.setLayout(vbox)
self.setStyleSheet("background-color:blue;")
self._dialog = NotifDialog(self)
log.addHandler(NotifDialogHandler(self._dialog))
log.info("test")
def moveEvent(self, event):
log.error("An error")
log.info("An info with an url http://test")
log.warning("A warning with a long long long longlong longlong longlong longlong longlong longlong longlong long message")
self._dialog.update()
def resizeEvent(self, event):
self._dialog.update()
main = MainWindow()
main.setMinimumWidth(600)
main.setMinimumHeight(600)
main.show()
exit_code = app.exec_()

View File

@@ -281,7 +281,7 @@ class HTTPClient(QtCore.QObject):
self._query_waiting_connections.append((request, callback))
# If we are not connected and we enqueue the first query we open the conection
if len(self._query_waiting_connections) == 1:
log.info("Connection to {}".format(self.url()))
log.debug("Connection to {}".format(self.url()))
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=5)
def _connectionError(self, callback, msg="", server=None):

View File

@@ -241,7 +241,7 @@ class LocalConfig(QtCore.QObject):
Read the configuration file.
"""
log.info("Load config from %s", config_path)
log.debug("Load config from %s", config_path)
try:
with open(config_path, "r", encoding="utf-8") as f:
self._last_config_changed = os.stat(config_path).st_mtime
@@ -268,7 +268,7 @@ class LocalConfig(QtCore.QObject):
with open(temporary, "w", encoding="utf-8") as f:
json.dump(self._settings, f, sort_keys=True, indent=4)
shutil.move(temporary, self._config_file)
log.info("Configuration save to %s", self._config_file)
log.debug("Configuration save to %s", self._config_file)
self._last_config_changed = os.stat(self._config_file).st_mtime
except (ValueError, OSError) as e:
log.error("Could not write the config file {}: {}".format(self._config_file, e))
@@ -373,9 +373,8 @@ class LocalConfig(QtCore.QObject):
self._settings[section] = settings
if changed:
log.info("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
log.debug("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
self.writeConfig()
return copy.deepcopy(settings)
def saveSectionSettings(self, section, settings):
@@ -391,7 +390,7 @@ class LocalConfig(QtCore.QObject):
if self._settings[section] != settings:
self._settings[section].update(copy.deepcopy(settings))
log.info("Section %s has changed. Saving configuration", section)
log.debug("Section %s has changed. Saving configuration", section)
self.writeConfig()
else:
log.debug("Section %s has not changed. Skip saving configuration", section)

View File

@@ -323,7 +323,7 @@ class LocalServer(QtCore.QObject):
return True
if self.isLocalServerRunning():
log.info("A local server already running on this host")
log.debug("A local server already running on this host")
# Try to kill the server. The server can be still running after
# if the server was started by hand
self._killAlreadyRunningServer()

View File

@@ -51,6 +51,7 @@ from .update_manager import UpdateManager
from .utils.analytics import AnalyticsClient
from .dialogs.appliance_wizard import ApplianceWizard
from .dialogs.new_appliance_dialog import NewApplianceDialog
from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler
from .registry.appliance import ApplianceError
log = logging.getLogger(__name__)
@@ -75,6 +76,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
super().__init__(parent)
self.setupUi(self)
# Setup logger
logging.getLogger().addHandler(NotifDialogHandler(NotifDialog(self)))
self._open_file_at_startup = open_file
MainWindow._instance = self

View File

@@ -255,6 +255,7 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
self.uiImagesPathLineEdit.setText(local_server["images_path"])
self.uiConfigsPathLineEdit.setText(local_server["configs_path"])
self.uiStatsCheckBox.setChecked(settings["send_stats"])
self.uiOverlayNotificationsCheckBox.setChecked(settings["overlay_notifications"])
self.uiCrashReportCheckBox.setChecked(local_server["report_errors"])
self.uiCheckForUpdateCheckBox.setChecked(settings["check_for_update"])
self.uiExperimentalFeaturesCheckBox.setChecked(settings["experimental_features"])
@@ -329,6 +330,7 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
"style": self.uiStyleComboBox.currentText(),
"experimental_features": self.uiExperimentalFeaturesCheckBox.isChecked(),
"check_for_update": self.uiCheckForUpdateCheckBox.isChecked(),
"overlay_notifications": self.uiOverlayNotificationsCheckBox.isChecked(),
"telnet_console_command": self.uiTelnetConsoleCommandLineEdit.text(),
"vnc_console_command": self.uiVNCConsoleCommandLineEdit.text(),
"delay_console_all": self.uiDelayConsoleAllSpinBox.value(),

View File

@@ -216,6 +216,7 @@ else:
GENERAL_SETTINGS = {
"style": DEFAULT_STYLE,
"check_for_update": True,
"overlay_notifications": True,
"experimental_features": False,
"send_stats": True,
"stats_visitor_id": str(uuid.uuid4()), # An anonymous id for stats

View File

@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>633</width>
<width>634</width>
<height>643</height>
</rect>
</property>
@@ -773,6 +773,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="uiOverlayNotificationsCheckBox">
<property name="text">
<string>Display error, warning and info in a overlay popup</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="uiExperimentalFeaturesCheckBox">
<property name="text">

View File

@@ -13,7 +13,7 @@ class Ui_GeneralPreferencesPageWidget(object):
def setupUi(self, GeneralPreferencesPageWidget):
GeneralPreferencesPageWidget.setObjectName("GeneralPreferencesPageWidget")
GeneralPreferencesPageWidget.resize(633, 643)
GeneralPreferencesPageWidget.resize(634, 643)
self.verticalLayout = QtWidgets.QVBoxLayout(GeneralPreferencesPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiMiscTabWidget = QtWidgets.QTabWidget(GeneralPreferencesPageWidget)
@@ -337,6 +337,9 @@ class Ui_GeneralPreferencesPageWidget(object):
self.uiStatsCheckBox.setChecked(True)
self.uiStatsCheckBox.setObjectName("uiStatsCheckBox")
self.verticalLayout_2.addWidget(self.uiStatsCheckBox)
self.uiOverlayNotificationsCheckBox = QtWidgets.QCheckBox(self.uiMiscTab)
self.uiOverlayNotificationsCheckBox.setObjectName("uiOverlayNotificationsCheckBox")
self.verticalLayout_2.addWidget(self.uiOverlayNotificationsCheckBox)
self.uiExperimentalFeaturesCheckBox = QtWidgets.QCheckBox(self.uiMiscTab)
self.uiExperimentalFeaturesCheckBox.setObjectName("uiExperimentalFeaturesCheckBox")
self.verticalLayout_2.addWidget(self.uiExperimentalFeaturesCheckBox)
@@ -415,6 +418,7 @@ class Ui_GeneralPreferencesPageWidget(object):
self.uiCheckForUpdateCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Automatically check for update"))
self.uiCrashReportCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Send anonymous crash reports"))
self.uiStatsCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Send anonymous usage statistics"))
self.uiOverlayNotificationsCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Display error, warning and info in a overlay popup"))
self.uiExperimentalFeaturesCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Enable experimental features (dangerous, restart required)"))
self.uiMultiProfilesCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Ask for settings profile at application startup (work profile / home profile)"))
self.uiMiscTabWidget.setTabText(self.uiMiscTabWidget.indexOf(self.uiMiscTab), _translate("GeneralPreferencesPageWidget", "Miscellaneous"))