diff --git a/gui/advancedview.cpp b/gui/advancedview.cpp new file mode 100644 index 0000000000..4dd47b6ade --- /dev/null +++ b/gui/advancedview.cpp @@ -0,0 +1,281 @@ +#include "advancedview.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +AdvancedView::AdvancedView(QIcon icon) +{ + m_icon = icon; + + advancedView = new QWidget(); + advancedView->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + + ClusterList clusters; + m_clusterModel = new ClusterModel(clusters); + + clusterListView = new QTableView(); + clusterListView->setModel(m_clusterModel); + clusterListView->setSelectionMode(QAbstractItemView::SingleSelection); + clusterListView->setSelectionBehavior(QAbstractItemView::SelectRows); + clusterListView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + clusterListView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + clusterListView->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + clusterListView->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents); + clusterListView->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents); + clusterListView->horizontalHeader()->setSectionResizeMode(5, QHeaderView::ResizeToContents); + clusterListView->horizontalHeader()->setSectionResizeMode(6, QHeaderView::ResizeToContents); + setSelectedClusterName("default"); + + startButton = new QPushButton(tr("Start")); + stopButton = new QPushButton(tr("Stop")); + pauseButton = new QPushButton(tr("Pause")); + deleteButton = new QPushButton(tr("Delete")); + refreshButton = new QPushButton(tr("Refresh")); + createButton = new QPushButton(tr("Create")); + sshButton = new QPushButton(tr("SSH")); + dashboardButton = new QPushButton(tr("Dashboard")); + basicButton = new QPushButton(tr("Basic View")); + + disableButtons(); + + QHBoxLayout *topButtonLayout = new QHBoxLayout; + topButtonLayout->addWidget(createButton); + topButtonLayout->addWidget(refreshButton); + topButtonLayout->addWidget(basicButton); + topButtonLayout->addSpacing(340); + + QHBoxLayout *bottomButtonLayout = new QHBoxLayout; + bottomButtonLayout->addWidget(startButton); + bottomButtonLayout->addWidget(stopButton); + bottomButtonLayout->addWidget(pauseButton); + bottomButtonLayout->addWidget(deleteButton); + bottomButtonLayout->addWidget(sshButton); + bottomButtonLayout->addWidget(dashboardButton); + + QVBoxLayout *clusterLayout = new QVBoxLayout; + clusterLayout->addLayout(topButtonLayout); + clusterLayout->addWidget(clusterListView); + clusterLayout->addLayout(bottomButtonLayout); + advancedView->setLayout(clusterLayout); + + QFont *loadingFont = new QFont(); + loadingFont->setPointSize(30); + loading = new QLabel("Loading..."); + loading->setFont(*loadingFont); + loading->setParent(clusterListView); + loading->setHidden(true); + + connect(startButton, &QAbstractButton::clicked, this, &AdvancedView::start); + connect(stopButton, &QAbstractButton::clicked, this, &AdvancedView::stop); + connect(pauseButton, &QAbstractButton::clicked, this, &AdvancedView::pause); + connect(deleteButton, &QAbstractButton::clicked, this, &AdvancedView::delete_); + connect(refreshButton, &QAbstractButton::clicked, this, &AdvancedView::refresh); + connect(createButton, &QAbstractButton::clicked, this, &AdvancedView::askName); + connect(sshButton, &QAbstractButton::clicked, this, &AdvancedView::ssh); + connect(dashboardButton, &QAbstractButton::clicked, this, &AdvancedView::dashboard); + connect(basicButton, &QAbstractButton::clicked, this, &AdvancedView::basic); +} + +static QString getPauseLabel(bool isPaused) +{ + if (isPaused) { + return "Unpause"; + } + return "Pause"; +} + +static QString getStartLabel(bool isRunning) +{ + if (isRunning) { + return "Reload"; + } + return "Start"; +} + +void AdvancedView::update(Cluster cluster) +{ + basicButton->setEnabled(true); + createButton->setEnabled(true); + refreshButton->setEnabled(true); + bool exists = !cluster.isEmpty(); + bool isRunning = cluster.status() == "Running"; + bool isPaused = cluster.status() == "Paused"; + startButton->setEnabled(exists); + stopButton->setEnabled(isRunning || isPaused); + pauseButton->setEnabled(isRunning || isPaused); + deleteButton->setEnabled(exists); + dashboardButton->setEnabled(isRunning); +#if __linux__ || __APPLE__ + sshButton->setEnabled(exists); +#else + basicSSHButton->setEnabled(false); +#endif + pauseButton->setText(getPauseLabel(isPaused)); + startButton->setText(getStartLabel(isRunning)); +} + +void AdvancedView::setSelectedClusterName(QString cluster) +{ + QAbstractItemModel *model = clusterListView->model(); + QModelIndex start = model->index(0, 0); + QModelIndexList index = model->match(start, Qt::DisplayRole, cluster); + if (index.size() == 0) { + return; + } + clusterListView->setCurrentIndex(index[0]); +} + +QString AdvancedView::selectedClusterName() +{ + QModelIndex index = clusterListView->currentIndex(); + QVariant variant = index.siblingAtColumn(0).data(Qt::DisplayRole); + if (variant.isNull()) { + return QString(); + } + return variant.toString(); +} + +void AdvancedView::updateClustersTable(ClusterList clusterList) +{ + QString cluster = selectedClusterName(); + m_clusterModel->setClusters(clusterList); + setSelectedClusterName(cluster); +} + +static int getCenter(int widgetSize, int parentSize) +{ + return parentSize / 2 - widgetSize / 2; +} + +void AdvancedView::showLoading() +{ + clusterListView->setEnabled(false); + loading->setHidden(false); + loading->raise(); + int width = getCenter(loading->width(), clusterListView->width()); + int height = getCenter(loading->height(), clusterListView->height()); + loading->move(width, height); +} + +void AdvancedView::hideLoading() +{ + loading->setHidden(true); + clusterListView->setEnabled(true); +} + +static QString profile = "minikube"; +static int cpus = 2; +static int memory = 2400; +static QString driver = ""; +static QString containerRuntime = ""; +static QString k8sVersion = ""; + +void AdvancedView::askName() +{ + QDialog dialog; + dialog.setWindowTitle(tr("Create minikube Cluster")); + dialog.setWindowIcon(m_icon); + dialog.setModal(true); + + QFormLayout form(&dialog); + QDialogButtonBox buttonBox(Qt::Horizontal, &dialog); + QLineEdit profileField(profile, &dialog); + form.addRow(new QLabel(tr("Profile")), &profileField); + buttonBox.addButton(QString(tr("Use Default Values")), QDialogButtonBox::AcceptRole); + connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + buttonBox.addButton(QString(tr("Set Custom Values")), QDialogButtonBox::RejectRole); + connect(&buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + form.addRow(&buttonBox); + + int code = dialog.exec(); + profile = profileField.text(); + if (code == QDialog::Accepted) { + QStringList args = { "-p", profile }; + emit createCluster(args); + } else if (code == QDialog::Rejected) { + askCustom(); + } +} + +void AdvancedView::askCustom() +{ + QDialog dialog; + dialog.setWindowTitle(tr("Set Cluster Values")); + dialog.setWindowIcon(m_icon); + dialog.setModal(true); + + QFormLayout form(&dialog); + QComboBox *driverComboBox = new QComboBox; + driverComboBox->addItems({ "docker", "virtualbox", "vmware", "podman" }); +#if __linux__ + driverComboBox->addItem("kvm2"); +#elif __APPLE__ + driverComboBox->addItems({ "hyperkit", "parallels" }); +#else + driverComboBox->addItem("hyperv"); +#endif + form.addRow(new QLabel(tr("Driver")), driverComboBox); + QComboBox *containerRuntimeComboBox = new QComboBox; + containerRuntimeComboBox->addItems({ "docker", "containerd", "crio" }); + form.addRow(new QLabel(tr("Container Runtime")), containerRuntimeComboBox); + QComboBox *k8sVersionComboBox = new QComboBox; + k8sVersionComboBox->addItems({ "stable", "latest", "none" }); + form.addRow(new QLabel(tr("Kubernetes Version")), k8sVersionComboBox); + QLineEdit cpuField(QString::number(cpus), &dialog); + form.addRow(new QLabel(tr("CPUs")), &cpuField); + QLineEdit memoryField(QString::number(memory), &dialog); + form.addRow(new QLabel(tr("Memory")), &memoryField); + + QDialogButtonBox buttonBox(Qt::Horizontal, &dialog); + buttonBox.addButton(QString(tr("Create")), QDialogButtonBox::AcceptRole); + connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + buttonBox.addButton(QString(tr("Cancel")), QDialogButtonBox::RejectRole); + connect(&buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + form.addRow(&buttonBox); + + int code = dialog.exec(); + if (code == QDialog::Accepted) { + driver = driverComboBox->itemText(driverComboBox->currentIndex()); + containerRuntime = + containerRuntimeComboBox->itemText(containerRuntimeComboBox->currentIndex()); + k8sVersion = k8sVersionComboBox->itemText(k8sVersionComboBox->currentIndex()); + if (k8sVersion == "none") { + k8sVersion = "v0.0.0"; + } + cpus = cpuField.text().toInt(); + memory = memoryField.text().toInt(); + QStringList args = { "-p", + profile, + "--driver", + driver, + "--container-runtime", + containerRuntime, + "--kubernetes-version", + k8sVersion, + "--cpus", + QString::number(cpus), + "--memory", + QString::number(memory) }; + emit createCluster(args); + } +} + +void AdvancedView::disableButtons() +{ + startButton->setEnabled(false); + stopButton->setEnabled(false); + pauseButton->setEnabled(false); + deleteButton->setEnabled(false); + sshButton->setEnabled(false); + dashboardButton->setEnabled(false); + basicButton->setEnabled(false); + createButton->setEnabled(false); + refreshButton->setEnabled(false); +} diff --git a/gui/advancedview.h b/gui/advancedview.h new file mode 100644 index 0000000000..93e4f0167a --- /dev/null +++ b/gui/advancedview.h @@ -0,0 +1,59 @@ +#ifndef ADVANCEDVIEW_H +#define ADVANCEDVIEW_H + +#include "cluster.h" + +#include +#include +#include +#include + +class AdvancedView : public QObject +{ + Q_OBJECT + +public: + explicit AdvancedView(QIcon icon); + QWidget *advancedView; + QTableView *clusterListView; + + QString selectedClusterName(); + void updateClustersTable(ClusterList clusters); + void showLoading(); + void hideLoading(); + void disableButtons(); + +public slots: + void update(Cluster cluster); + +signals: + void start(); + void stop(); + void pause(); + void delete_(); + void refresh(); + void ssh(); + void dashboard(); + void basic(); + void createCluster(QStringList args); + +private: + void setSelectedClusterName(QString cluster); + void askName(); + void askCustom(); + + QPushButton *startButton; + QPushButton *stopButton; + QPushButton *pauseButton; + QPushButton *deleteButton; + QPushButton *refreshButton; + QPushButton *sshButton; + QPushButton *dashboardButton; + QPushButton *basicButton; + QPushButton *createButton; + QLabel *loading; + ClusterModel *m_clusterModel; + QIcon m_icon; +}; + +#endif // ADVANCEDVIEW_H diff --git a/gui/basicview.cpp b/gui/basicview.cpp new file mode 100644 index 0000000000..ff31b20345 --- /dev/null +++ b/gui/basicview.cpp @@ -0,0 +1,90 @@ +#include "basicview.h" + +#include + +BasicView::BasicView() +{ + basicView = new QWidget(); + + startButton = new QPushButton(tr("Start")); + stopButton = new QPushButton(tr("Stop")); + pauseButton = new QPushButton(tr("Pause")); + deleteButton = new QPushButton(tr("Delete")); + refreshButton = new QPushButton(tr("Refresh")); + sshButton = new QPushButton(tr("SSH")); + dashboardButton = new QPushButton(tr("Dashboard")); + advancedButton = new QPushButton(tr("Advanced View")); + + disableButtons(); + + QVBoxLayout *buttonLayout = new QVBoxLayout; + basicView->setLayout(buttonLayout); + buttonLayout->addWidget(startButton); + buttonLayout->addWidget(stopButton); + buttonLayout->addWidget(pauseButton); + buttonLayout->addWidget(deleteButton); + buttonLayout->addWidget(refreshButton); + buttonLayout->addWidget(sshButton); + buttonLayout->addWidget(dashboardButton); + buttonLayout->addWidget(advancedButton); + basicView->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + + connect(startButton, &QPushButton::clicked, this, &BasicView::start); + connect(stopButton, &QAbstractButton::clicked, this, &BasicView::stop); + connect(pauseButton, &QAbstractButton::clicked, this, &BasicView::pause); + connect(deleteButton, &QAbstractButton::clicked, this, &BasicView::delete_); + connect(refreshButton, &QAbstractButton::clicked, this, &BasicView::refresh); + connect(sshButton, &QAbstractButton::clicked, this, &BasicView::ssh); + connect(dashboardButton, &QAbstractButton::clicked, this, &BasicView::dashboard); + connect(advancedButton, &QAbstractButton::clicked, this, &BasicView::advanced); +} + +static QString getPauseLabel(bool isPaused) +{ + if (isPaused) { + return "Unpause"; + } + return "Pause"; +} + +static QString getStartLabel(bool isRunning) +{ + if (isRunning) { + return "Reload"; + } + return "Start"; +} + +void BasicView::update(Cluster cluster) +{ + + startButton->setEnabled(true); + advancedButton->setEnabled(true); + refreshButton->setEnabled(true); + bool exists = !cluster.isEmpty(); + bool isRunning = cluster.status() == "Running"; + bool isPaused = cluster.status() == "Paused"; + stopButton->setEnabled(isRunning || isPaused); + pauseButton->setEnabled(isRunning || isPaused); + deleteButton->setEnabled(exists); + dashboardButton->setEnabled(isRunning); +#if __linux__ || __APPLE__ + sshButton->setEnabled(exists); +#else + basicSSHButton->setEnabled(false); +#endif + pauseButton->setText(getPauseLabel(isPaused)); + startButton->setText(getStartLabel(isRunning)); +} + +void BasicView::disableButtons() +{ + startButton->setEnabled(false); + stopButton->setEnabled(false); + deleteButton->setEnabled(false); + pauseButton->setEnabled(false); + sshButton->setEnabled(false); + dashboardButton->setEnabled(false); + advancedButton->setEnabled(false); + refreshButton->setEnabled(false); +} diff --git a/gui/basicview.h b/gui/basicview.h new file mode 100644 index 0000000000..4d93e62c4d --- /dev/null +++ b/gui/basicview.h @@ -0,0 +1,40 @@ +#ifndef BASICVIEW_H +#define BASICVIEW_H + +#include "cluster.h" + +#include +#include + +class BasicView : public QObject +{ + Q_OBJECT + +public: + explicit BasicView(); + QWidget *basicView; + void update(Cluster cluster); + void disableButtons(); + +signals: + void start(); + void stop(); + void pause(); + void delete_(); + void refresh(); + void ssh(); + void dashboard(); + void advanced(); + +private: + QPushButton *startButton; + QPushButton *stopButton; + QPushButton *pauseButton; + QPushButton *deleteButton; + QPushButton *refreshButton; + QPushButton *sshButton; + QPushButton *dashboardButton; + QPushButton *advancedButton; +}; + +#endif // BASICVIEW_H diff --git a/gui/commandrunner.cpp b/gui/commandrunner.cpp new file mode 100644 index 0000000000..e4023338a4 --- /dev/null +++ b/gui/commandrunner.cpp @@ -0,0 +1,212 @@ +#include "commandrunner.h" + +#include +#include +#include +#include +#include + +CommandRunner::CommandRunner(QDialog *parent) +{ + m_env = QProcessEnvironment::systemEnvironment(); + m_parent = parent; + minikubePath(); +#if __APPLE__ + setMinikubePath(); +#endif +} + +void CommandRunner::executeMinikubeCommand(QStringList args) +{ + m_output = ""; + QStringList userArgs = { "--user", "minikube-gui" }; + args << userArgs; + m_process = new QProcess(m_parent); + connect(m_process, QOverload::of(&QProcess::finished), this, &CommandRunner::executionCompleted); + connect(m_process, &QProcess::readyReadStandardError, this, &CommandRunner::errorReady); + connect(m_process, &QProcess::readyReadStandardOutput, this, &CommandRunner::outputReady); + m_process->setProcessEnvironment(m_env); + m_process->start(m_minikubePath, args); + emit CommandRunner::startingExecution(); +} + +void CommandRunner::startMinikube(QStringList args) +{ + m_command = "start"; + QStringList baseArgs = { "start", "-o", "json" }; + baseArgs << args; + m_args = baseArgs; + executeMinikubeCommand(baseArgs); + emit startCommandStarting(); +} + +void CommandRunner::stopMinikube(QStringList args) +{ + QStringList baseArgs = { "stop" }; + baseArgs << args; + executeMinikubeCommand(baseArgs); +} + +void CommandRunner::pauseMinikube(QStringList args) +{ + QStringList baseArgs = { "pause" }; + baseArgs << args; + executeMinikubeCommand(baseArgs); +} + +void CommandRunner::unpauseMinikube(QStringList args) +{ + QStringList baseArgs = { "unpause" }; + baseArgs << args; + executeMinikubeCommand(baseArgs); +} + +void CommandRunner::deleteMinikube(QStringList args) +{ + m_command = "delete"; + QStringList baseArgs = { "delete" }; + baseArgs << args; + executeMinikubeCommand(baseArgs); +} + +void CommandRunner::stopCommand() +{ + m_process->terminate(); +} + +static Cluster createClusterObject(QJsonObject obj) +{ + QString name; + if (obj.contains("Name")) { + name = obj["Name"].toString(); + } + Cluster cluster(name); + if (obj.contains("Status")) { + QString status = obj["Status"].toString(); + cluster.setStatus(status); + } + if (!obj.contains("Config")) { + return cluster; + } + QJsonObject config = obj["Config"].toObject(); + if (config.contains("CPUs")) { + int cpus = config["CPUs"].toInt(); + cluster.setCpus(cpus); + } + if (config.contains("Memory")) { + int memory = config["Memory"].toInt(); + cluster.setMemory(memory); + } + if (config.contains("Driver")) { + QString driver = config["Driver"].toString(); + cluster.setDriver(driver); + } + if (!config.contains("KubernetesConfig")) { + return cluster; + } + QJsonObject k8sConfig = config["KubernetesConfig"].toObject(); + if (k8sConfig.contains("ContainerRuntime")) { + QString containerRuntime = k8sConfig["ContainerRuntime"].toString(); + cluster.setContainerRuntime(containerRuntime); + } + if (k8sConfig.contains("KubernetesVersion")) { + QString k8sVersion = k8sConfig["KubernetesVersion"].toString(); + cluster.setK8sVersion(k8sVersion); + } + return cluster; +} + +static ClusterList jsonToClusterList(QString text) +{ + ClusterList clusters; + QStringList lines; +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + lines = text.split("\n", Qt::SkipEmptyParts); +#else + lines = text.split("\n", QString::SkipEmptyParts); +#endif + for (int i = 0; i < lines.size(); i++) { + QString line = lines.at(i); + QJsonParseError error; + QJsonDocument json = QJsonDocument::fromJson(line.toUtf8(), &error); + if (json.isNull()) { + qDebug() << error.errorString(); + continue; + } + if (!json.isObject()) { + continue; + } + QJsonObject par = json.object(); + QJsonArray valid = par["valid"].toArray(); + QJsonArray invalid = par["invalid"].toArray(); + for (int i = 0; i < valid.size(); i++) { + QJsonObject obj = valid[i].toObject(); + Cluster cluster = createClusterObject(obj); + clusters << cluster; + } + for (int i = 0; i < invalid.size(); i++) { + QJsonObject obj = invalid[i].toObject(); + Cluster cluster = createClusterObject(obj); + cluster.setStatus("Invalid"); + clusters << cluster; + } + } + return clusters; +} + +void CommandRunner::requestClusters() +{ + m_command = "cluster"; + QStringList args = { "profile", "list", "-o", "json" }; + executeMinikubeCommand(args); +} + +void CommandRunner::executionCompleted() +{ + QString cmd = m_command; + m_command = ""; + QString output = m_output; + int exitCode = m_process->exitCode(); + delete m_process; + if (cmd != "cluster") { + emit executionEnded(); + } + if (cmd == "start" && exitCode != 0) { + emit error(m_args, output); + } + if (cmd == "cluster") { + ClusterList clusterList = jsonToClusterList(output); + emit updatedClusters(clusterList); + } +} + +void CommandRunner::errorReady() +{ + QString text = m_process->readAllStandardError(); + m_output.append(text); + emit output(text); +} + +void CommandRunner::outputReady() +{ + QString text = m_process->readAllStandardOutput(); + m_output.append(text); + emit output(text); +} + +void CommandRunner::setMinikubePath() +{ + m_env = QProcessEnvironment::systemEnvironment(); + QString path = m_env.value("PATH") + ":/usr/local/bin"; + m_env.insert("PATH", path); +} + +void CommandRunner::minikubePath() +{ + m_minikubePath = QStandardPaths::findExecutable("minikube"); + if (!m_minikubePath.isEmpty()) { + return; + } + QStringList path = { "/usr/local/bin" }; + m_minikubePath = QStandardPaths::findExecutable("minikube", path); +} diff --git a/gui/commandrunner.h b/gui/commandrunner.h new file mode 100644 index 0000000000..ddf8cfbaca --- /dev/null +++ b/gui/commandrunner.h @@ -0,0 +1,58 @@ +#ifndef COMMANDRUNNER_H +#define COMMANDRUNNER_H + +#include "cluster.h" + +#include +#include +#include +#include +#include +#include +#include + +class CommandRunner : public QObject +{ + Q_OBJECT + +public: + CommandRunner(QDialog *parent); + + void startMinikube(QStringList args); + void stopMinikube(QStringList args); + void pauseMinikube(QStringList args); + void unpauseMinikube(QStringList args); + void deleteMinikube(QStringList args); + void stopCommand(); + void requestClusters(); + +signals: + void startingExecution(); + void executionEnded(); + void output(QString text); + void error(QStringList args, QString text); + void updatedClusters(ClusterList clusterList); + void startCommandStarting(); + +private slots: + void executionCompleted(); + void outputReady(); + void errorReady(); + +private: + void executeMinikubeCommand(QStringList args); + void minikubePath(); +#if __APPLE__ + void setMinikubePath(); +#endif + + QProcess *m_process; + QProcessEnvironment m_env; + QString m_output; + QString m_minikubePath; + QString m_command; + QDialog *m_parent; + QStringList m_args; +}; + +#endif // COMMANDRUNNER_H diff --git a/gui/errormessage.cpp b/gui/errormessage.cpp new file mode 100644 index 0000000000..375eb77eee --- /dev/null +++ b/gui/errormessage.cpp @@ -0,0 +1,63 @@ +#include "errormessage.h" + +#include +#include +#include +#include +#include +#include + +ErrorMessage::ErrorMessage(QDialog *parent, QIcon icon) +{ + m_parent = parent; + m_icon = icon; +} + +void ErrorMessage::error(QString errorCode, QString advice, QString message, QString url, QString issues) +{ + + m_dialog = new QDialog(m_parent); + m_dialog->setWindowTitle(tr("minikube start failed")); + m_dialog->setWindowIcon(m_icon); + m_dialog->setFixedWidth(600); + m_dialog->setModal(true); + QFormLayout form(m_dialog); + createLabel("Error Code", errorCode, &form, false); + createLabel("Advice", advice, &form, false); + QTextEdit *errorMessage = new QTextEdit(); + errorMessage->setText(message); + errorMessage->setWordWrapMode(QTextOption::WrapAnywhere); + int pointSize = errorMessage->font().pointSize(); + errorMessage->setFont(QFont("Courier", pointSize)); + errorMessage->setAutoFillBackground(true); + errorMessage->setReadOnly(true); + form.addRow(errorMessage); + createLabel("Link to documentation", url, &form, true); + createLabel("Link to related issue", issues, &form, true); + QLabel *fileLabel = new QLabel(); + fileLabel->setOpenExternalLinks(true); + fileLabel->setWordWrap(true); + QString logFile = QDir::homePath() + "/.minikube/logs/lastStart.txt"; + fileLabel->setText("View log file"); + form.addRow(fileLabel); + QDialogButtonBox buttonBox(Qt::Horizontal, m_dialog); + buttonBox.addButton(QString(tr("OK")), QDialogButtonBox::AcceptRole); + connect(&buttonBox, &QDialogButtonBox::accepted, m_dialog, &QDialog::accept); + form.addRow(&buttonBox); + m_dialog->exec(); +} + +QLabel *ErrorMessage::createLabel(QString title, QString text, QFormLayout *form, bool isLink) +{ + QLabel *label = new QLabel(); + if (!text.isEmpty()) { + form->addRow(label); + } + if (isLink) { + label->setOpenExternalLinks(true); + text = "" + text + ""; + } + label->setWordWrap(true); + label->setText(title + ": " + text); + return label; +} diff --git a/gui/errormessage.h b/gui/errormessage.h new file mode 100644 index 0000000000..dfab8e1db6 --- /dev/null +++ b/gui/errormessage.h @@ -0,0 +1,26 @@ +#ifndef ERRORMESSAGE_H +#define ERRORMESSAGE_H + +#include +#include +#include +#include +#include + +class ErrorMessage : public QObject +{ + Q_OBJECT + +public: + explicit ErrorMessage(QDialog *parent, QIcon icon); + + void error(QString errorCode, QString advice, QString errorMessage, QString url, QString issues); + QLabel *createLabel(QString title, QString text, QFormLayout *form, bool isLink); + +private: + QDialog *m_dialog; + QIcon m_icon; + QDialog *m_parent; +}; + +#endif // ERRORMESSAGE_H diff --git a/gui/hyperkit.cpp b/gui/hyperkit.cpp new file mode 100644 index 0000000000..01d8784760 --- /dev/null +++ b/gui/hyperkit.cpp @@ -0,0 +1,56 @@ +#include "hyperkit.h" + +#include +#include + +HyperKit::HyperKit(QIcon icon) +{ + m_icon = icon; +} + +#if __APPLE__ +bool HyperKit::hyperkitPermissionFix(QStringList args, QString text) +{ + if (!text.contains("docker-machine-driver-hyperkit needs to run with elevated permissions")) { + return false; + } + if (!showHyperKitMessage()) { + return false; + } + + hyperkitPermission(); + emit rerun(args); +} + +void HyperKit::hyperkitPermission() +{ + QString command = "sudo chown root:wheel ~/.minikube/bin/docker-machine-driver-hyperkit && " + "sudo chmod u+s ~/.minikube/bin/docker-machine-driver-hyperkit && exit"; + QStringList arguments = { "-e", "tell app \"Terminal\"", + "-e", "set w to do script \"" + command + "\"", + "-e", "activate", + "-e", "repeat", + "-e", "delay 0.1", + "-e", "if not busy of w then exit repeat", + "-e", "end repeat", + "-e", "end tell" }; + QProcess *process = new QProcess(); + process->start("/usr/bin/osascript", arguments); + process->waitForFinished(-1); +} + +bool HyperKit::showHyperKitMessage() +{ + QMessageBox msgBox; + msgBox.setWindowTitle("HyperKit Permissions Required"); + msgBox.setWindowIcon(m_icon); + msgBox.setModal(true); + msgBox.setText("The HyperKit driver requires a one-time sudo permission.\n\nIf you'd like " + "to proceed, press OK and then enter your password into the terminal prompt, " + "the start will resume after."); + msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Ok); + int code = msgBox.exec(); + return code == QMessageBox::Ok; +} +#endif diff --git a/gui/hyperkit.h b/gui/hyperkit.h new file mode 100644 index 0000000000..94a768b5a5 --- /dev/null +++ b/gui/hyperkit.h @@ -0,0 +1,25 @@ +#ifndef HYPERKIT_H +#define HYPERKIT_H + +#include +#include +#include + +class HyperKit : public QObject +{ + Q_OBJECT + +public: + explicit HyperKit(QIcon icon); + bool hyperkitPermissionFix(QStringList args, QString text); + +signals: + void rerun(QStringList args); + +private: + void hyperkitPermission(); + bool showHyperKitMessage(); + QIcon m_icon; +}; + +#endif // HYPERKIT_H diff --git a/gui/operator.cpp b/gui/operator.cpp new file mode 100644 index 0000000000..61b7b4918e --- /dev/null +++ b/gui/operator.cpp @@ -0,0 +1,363 @@ +#include "operator.h" + +#include +#include +#include +#include + +Operator::Operator(AdvancedView *advancedView, BasicView *basicView, CommandRunner *commandRunner, ErrorMessage *errorMessage, ProgressWindow *progressWindow, Tray *tray, HyperKit *hyperKit, Updater *updater, QStackedWidget *stackedWidget, QDialog *parent) +{ + m_advancedView = advancedView; + m_basicView = basicView; + m_commandRunner = commandRunner; + m_errorMessage = errorMessage; + m_progressWindow = progressWindow; + m_tray = tray; + m_hyperKit = hyperKit; + m_updater = updater; + m_stackedWidget = stackedWidget; + m_parent = parent; + m_isBasicView = true; + + connect(m_basicView, &BasicView::start, this, &Operator::startMinikube); + connect(m_basicView, &BasicView::stop, this, &Operator::stopMinikube); + connect(m_basicView, &BasicView::pause, this, &Operator::pauseOrUnpauseMinikube); + connect(m_basicView, &BasicView::delete_, this, &Operator::deleteMinikube); + connect(m_basicView, &BasicView::refresh, this, &Operator::updateClusters); + connect(m_basicView, &BasicView::ssh, this, &Operator::sshConsole); + connect(m_basicView, &BasicView::dashboard, this, &Operator::dashboardBrowser); + connect(m_basicView, &BasicView::advanced, this, &Operator::toAdvancedView); + + connect(m_advancedView, &AdvancedView::start, this, &Operator::startMinikube); + connect(m_advancedView, &AdvancedView::stop, this, &Operator::stopMinikube); + connect(m_advancedView, &AdvancedView::pause, this, &Operator::pauseOrUnpauseMinikube); + connect(m_advancedView, &AdvancedView::delete_, this, &Operator::deleteMinikube); + connect(m_advancedView, &AdvancedView::refresh, this, &Operator::updateClusters); + connect(m_advancedView, &AdvancedView::ssh, this, &Operator::sshConsole); + connect(m_advancedView, &AdvancedView::dashboard, this, &Operator::dashboardBrowser); + connect(m_advancedView, &AdvancedView::basic, this, &Operator::toBasicView); + connect(m_advancedView, &AdvancedView::createCluster, this, &Operator::createCluster); + connect(m_advancedView->clusterListView, SIGNAL(clicked(QModelIndex)), this, SLOT(updateButtons())); + + connect(m_commandRunner, &CommandRunner::startingExecution, this, &Operator::commandStarting); + connect(m_commandRunner, &CommandRunner::executionEnded, this, &Operator::commandEnding); + connect(m_commandRunner, &CommandRunner::output, this, &Operator::commandOutput); + connect(m_commandRunner, &CommandRunner::error, this, &Operator::commandError); + connect(m_commandRunner, &CommandRunner::updatedClusters, this, &Operator::clustersReceived); + connect(m_commandRunner, &CommandRunner::startCommandStarting, this, &Operator::startCommandStarting); + + connect(m_progressWindow, &ProgressWindow::cancelled, this, &Operator::cancelCommand); + + connect(m_tray, &Tray::restoreWindow, this, &Operator::restoreWindow); + connect(m_tray, &Tray::hideWindow, this, &Operator::hideWindow); + connect(m_tray, &Tray::start, this, &Operator::startMinikube); + connect(m_tray, &Tray::stop, this, &Operator::stopMinikube); + connect(m_tray, &Tray::pauseOrUnpause, this, &Operator::pauseOrUnpauseMinikube); + + connect(m_hyperKit, &HyperKit::rerun, this, &Operator::createCluster); + + updateClusters(); +} + +QStringList Operator::getCurrentClusterFlags() +{ + return { "-p", selectedClusterName() }; +} + +void Operator::startMinikube() +{ + m_commandRunner->startMinikube(getCurrentClusterFlags()); +} + +void Operator::stopMinikube() +{ + m_commandRunner->stopMinikube(getCurrentClusterFlags()); +} + +void Operator::pauseOrUnpauseMinikube() +{ + Cluster cluster = selectedCluster(); + if (cluster.status() == "Paused") { + unpauseMinikube(); + return; + } + pauseMinikube(); +} + +void Operator::pauseMinikube() +{ + m_commandRunner->pauseMinikube(getCurrentClusterFlags()); +} + +void Operator::unpauseMinikube() +{ + m_commandRunner->unpauseMinikube(getCurrentClusterFlags()); +} + +void Operator::deleteMinikube() +{ + m_commandRunner->deleteMinikube(getCurrentClusterFlags()); +} + +void Operator::createCluster(QStringList args) +{ + m_commandRunner->startMinikube(args); +} + +void Operator::startCommandStarting() +{ + commandStarting(); + m_progressWindow->setText("Starting..."); + m_progressWindow->show(); +} + +void Operator::commandStarting() +{ + m_advancedView->showLoading(); + m_tray->disableActions(); + m_parent->setCursor(Qt::WaitCursor); + disableButtons(); +} + +void Operator::disableButtons() +{ + if (m_isBasicView) { + m_basicView->disableButtons(); + } else { + m_advancedView->disableButtons(); + } +} + +void Operator::commandEnding() +{ + m_progressWindow->done(); + updateClusters(); +} + +void Operator::toAdvancedView() +{ + m_isBasicView = false; + m_stackedWidget->setCurrentIndex(1); + m_parent->resize(670, 400); + updateButtons(); +} + +void Operator::toBasicView() +{ + m_isBasicView = true; + m_stackedWidget->setCurrentIndex(0); + m_parent->resize(200, 275); + updateButtons(); +} + +void Operator::updateClusters() +{ + m_commandRunner->requestClusters(); +} + +void Operator::clustersReceived(ClusterList clusterList) +{ + m_clusterList = clusterList; + m_advancedView->updateClustersTable(m_clusterList); + updateButtons(); + m_advancedView->hideLoading(); + m_parent->unsetCursor(); + m_updater->checkForUpdates(); +} + +void Operator::updateButtons() +{ + Cluster cluster = selectedCluster(); + if (m_isBasicView) { + m_basicView->update(cluster); + } else { + m_advancedView->update(cluster); + } + m_tray->updateTrayActions(cluster); + m_tray->updateStatus(cluster); +} + +void Operator::restoreWindow() +{ + bool wasVisible = m_parent->isVisible(); + m_parent->showNormal(); + m_parent->activateWindow(); + if (wasVisible) { + return; + } + updateClusters(); +} + +void Operator::hideWindow() +{ + m_parent->hide(); +} + +void Operator::commandOutput(QString text) +{ + QStringList lines; +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + lines = text.split("\n", Qt::SkipEmptyParts); +#else + lines = text.split("\n", QString::SkipEmptyParts); +#endif + for (int i = 0; i < lines.size(); i++) { + QJsonDocument json = QJsonDocument::fromJson(lines[i].toUtf8()); + QJsonObject object = json.object(); + QString type = object["type"].toString(); + if (type != "io.k8s.sigs.minikube.step") { + return; + } + QJsonObject data = object["data"].toObject(); + QString stringStep = data["currentstep"].toString(); + int currStep = stringStep.toInt(); + QString totalString = data["totalsteps"].toString(); + int totalSteps = totalString.toInt(); + QString message = data["message"].toString(); + m_progressWindow->setBarMaximum(totalSteps); + m_progressWindow->setBarValue(currStep); + m_progressWindow->setText(message); + } +} + +void Operator::commandError(QStringList args, QString text) +{ +#if __APPLE__ + if (m_hyperKit->hyperkitPermissionFix(args, text)) { + return; + } +#endif + QStringList lines; +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + lines = text.split("\n", Qt::SkipEmptyParts); +#else + lines = text.split("\n", QString::SkipEmptyParts); +#endif + for (int i = 0; i < lines.size(); i++) { + QString line = lines.at(i); + QJsonParseError error; + QJsonDocument json = QJsonDocument::fromJson(line.toUtf8(), &error); + if (json.isNull() || !json.isObject()) { + continue; + } + QJsonObject par = json.object(); + QJsonObject data = par["data"].toObject(); + if (!data.contains("exitcode")) { + continue; + } + QString advice = data["advice"].toString(); + QString message = data["message"].toString(); + QString name = data["name"].toString(); + QString url = data["url"].toString(); + QString issues = data["issues"].toString(); + + m_errorMessage->error(name, advice, message, url, issues); + break; + } +} + +void Operator::cancelCommand() +{ + m_commandRunner->stopCommand(); +} + +QString Operator::selectedClusterName() +{ + if (m_isBasicView) { + return "minikube"; + } + return m_advancedView->selectedClusterName(); +} + +Cluster Operator::selectedCluster() +{ + QString clusterName = selectedClusterName(); + if (clusterName.isEmpty()) { + return Cluster(); + } + ClusterList clusters = m_clusterList; + ClusterHash clusterHash; + for (int i = 0; i < clusters.size(); i++) { + Cluster cluster = clusters.at(i); + clusterHash[cluster.name()] = cluster; + } + return clusterHash[clusterName]; +} + +static QString minikubePath() +{ + QString minikubePath = QStandardPaths::findExecutable("minikube"); + if (!minikubePath.isEmpty()) { + return minikubePath; + } + QStringList path = { "/usr/local/bin" }; + return QStandardPaths::findExecutable("minikube", path); +} + +void Operator::sshConsole() +{ + QString program = minikubePath(); +#ifndef QT_NO_TERMWIDGET + QMainWindow *mainWindow = new QMainWindow(); + int startnow = 0; // set shell program first + + QTermWidget *console = new QTermWidget(startnow); + + QFont font = QApplication::font(); + font.setFamily("Monospace"); + font.setPointSize(10); + + console->setTerminalFont(font); + console->setColorScheme("Tango"); + console->setShellProgram(program); + QStringList args = { "ssh", "-p", selectedClusterName() }; + console->setArgs(args); + console->startShellProgram(); + + QObject::connect(console, SIGNAL(finished()), mainWindow, SLOT(close())); + + mainWindow->setWindowTitle(nameLabel->text()); + mainWindow->resize(800, 400); + mainWindow->setCentralWidget(console); + mainWindow->show(); +#elif __APPLE__ + QString command = program + " ssh -p " + selectedClusterName(); + QStringList arguments = { "-e", "tell app \"Terminal\"", "-e", "activate", + "-e", "do script \"" + command + "\"", "-e", "end tell" }; + QProcess *process = new QProcess(this); + process->start("/usr/bin/osascript", arguments); +#else + QString terminal = qEnvironmentVariable("TERMINAL"); + if (terminal.isEmpty()) { + terminal = "x-terminal-emulator"; + if (QStandardPaths::findExecutable(terminal).isEmpty()) { + terminal = "xterm"; + } + } + + QStringList arguments = { "-e", QString("%1 ssh -p %2").arg(program, selectedClusterName()) }; + QProcess *process = new QProcess(this); + process->start(QStandardPaths::findExecutable(terminal), arguments); +#endif +} + +void Operator::dashboardBrowser() +{ + dashboardClose(); + + QString program = minikubePath(); + QProcess *process = new QProcess(this); + QStringList arguments = { "dashboard", "-p", selectedClusterName() }; + process->start(program, arguments); + + dashboardProcess = process; + dashboardProcess->waitForStarted(); +} + +void Operator::dashboardClose() +{ + if (dashboardProcess) { + dashboardProcess->terminate(); + dashboardProcess->waitForFinished(); + } +} diff --git a/gui/operator.h b/gui/operator.h new file mode 100644 index 0000000000..eb1490c423 --- /dev/null +++ b/gui/operator.h @@ -0,0 +1,71 @@ +#ifndef OPERATOR_H +#define OPERATOR_H + +#include "advancedview.h" +#include "basicview.h" +#include "cluster.h" +#include "commandrunner.h" +#include "errormessage.h" +#include "progresswindow.h" +#include "tray.h" +#include "hyperkit.h" +#include "updater.h" + +#include + +class Operator : public QObject +{ + Q_OBJECT + +public: + Operator(AdvancedView *advancedView, BasicView *basicView, CommandRunner *commandRunner, ErrorMessage *errorMessage, ProgressWindow *progressWindow, Tray *tray, HyperKit *hyperKit, Updater *updater, QStackedWidget *stackedWidget, QDialog *parent); + +public slots: + void startMinikube(); + void stopMinikube(); + void pauseOrUnpauseMinikube(); + void deleteMinikube(); + +private slots: + void commandStarting(); + void commandEnding(); + void commandOutput(QString text); + void commandError(QStringList args, QString text); + void cancelCommand(); + void toBasicView(); + void toAdvancedView(); + void createCluster(QStringList args); + void updateButtons(); + void clustersReceived(ClusterList clusterList); + void startCommandStarting(); + +private: + QStringList getCurrentClusterFlags(); + void updateClusters(); + QString selectedClusterName(); + Cluster selectedCluster(); + void sshConsole(); + void dashboardBrowser(); + void dashboardClose(); + void pauseMinikube(); + void unpauseMinikube(); + void restoreWindow(); + void hideWindow(); + void disableButtons(); + + AdvancedView *m_advancedView; + BasicView *m_basicView; + CommandRunner *m_commandRunner; + ErrorMessage *m_errorMessage; + ProgressWindow *m_progressWindow; + ClusterList m_clusterList; + Tray *m_tray; + HyperKit *m_hyperKit; + Updater *m_updater; + bool m_isBasicView; + QProcess *dashboardProcess; + QStackedWidget *m_stackedWidget; + QDialog *m_parent; +}; + +#endif // OPERATOR_H diff --git a/gui/progresswindow.cpp b/gui/progresswindow.cpp new file mode 100644 index 0000000000..ae8da892fe --- /dev/null +++ b/gui/progresswindow.cpp @@ -0,0 +1,65 @@ +#include "progresswindow.h" + +#include +#include + +ProgressWindow::ProgressWindow(QWidget *parent, QIcon icon) +{ + m_icon = icon; + + m_dialog = new QDialog(parent); + m_dialog->setWindowIcon(m_icon); + m_dialog->resize(300, 150); + m_dialog->setWindowFlags(Qt::FramelessWindowHint); + m_dialog->setModal(true); + + QVBoxLayout form(m_dialog); + + m_text = new QLabel(); + m_text->setWordWrap(true); + form.addWidget(m_text); + + m_progressBar = new QProgressBar(); + form.addWidget(m_progressBar); + + m_cancelButton = new QPushButton(); + m_cancelButton->setText(tr("Cancel")); + connect(m_cancelButton, &QAbstractButton::clicked, this, &ProgressWindow::cancel); + form.addWidget(m_cancelButton); + + // if the dialog isn't opened now it breaks formatting + m_dialog->open(); + m_dialog->hide(); +} + +void ProgressWindow::setBarMaximum(int max) +{ + m_progressBar->setMaximum(max); +} + +void ProgressWindow::setBarValue(int value) +{ + m_progressBar->setValue(value); +} + +void ProgressWindow::setText(QString text) +{ + m_text->setText(text); +} + +void ProgressWindow::show() +{ + m_dialog->open(); +} + +void ProgressWindow::cancel() +{ + done(); + emit cancelled(); +} + +void ProgressWindow::done() +{ + m_dialog->hide(); + m_progressBar->setValue(0); +} diff --git a/gui/progresswindow.h b/gui/progresswindow.h new file mode 100644 index 0000000000..7d016f5022 --- /dev/null +++ b/gui/progresswindow.h @@ -0,0 +1,38 @@ +#ifndef PROGRESSWINDOW_H +#define PROGRESSWINDOW_H + +#include +#include +#include +#include +#include +#include +#include + +class ProgressWindow : public QObject +{ + Q_OBJECT + +public: + explicit ProgressWindow(QWidget *parent, QIcon icon); + + void setBarMaximum(int max); + void setBarValue(int value); + void setText(QString text); + void show(); + void done(); + +signals: + void cancelled(); + +private: + void cancel(); + + QDialog *m_dialog; + QLabel *m_text; + QProgressBar *m_progressBar; + QPushButton *m_cancelButton; + QIcon m_icon; +}; + +#endif // PROGRESSWINDOW_H diff --git a/gui/systray.pro b/gui/systray.pro index 45c3639daf..9430a3673e 100644 --- a/gui/systray.pro +++ b/gui/systray.pro @@ -1,7 +1,25 @@ HEADERS = window.h \ - cluster.h + advancedview.h \ + basicview.h \ + cluster.h \ + commandrunner.h \ + errormessage.h \ + hyperkit.h \ + operator.h \ + progresswindow.h \ + tray.h \ + updater.h SOURCES = main.cpp \ + advancedview.cpp \ + basicview.cpp \ cluster.cpp \ + commandrunner.cpp \ + errormessage.cpp \ + hyperkit.cpp \ + operator.cpp \ + progresswindow.cpp \ + tray.cpp \ + updater.cpp \ window.cpp RESOURCES = systray.qrc ICON = images/minikube.icns diff --git a/gui/tray.cpp b/gui/tray.cpp new file mode 100644 index 0000000000..8768a4226b --- /dev/null +++ b/gui/tray.cpp @@ -0,0 +1,115 @@ +#include "tray.h" + +#include +#include +#include + +Tray::Tray(QIcon icon) +{ + m_icon = icon; + + trayIconMenu = new QMenu(); + trayIcon = new QSystemTrayIcon(this); + + connect(trayIcon, &QSystemTrayIcon::activated, this, &Tray::iconActivated); + + minimizeAction = new QAction(tr("Mi&nimize"), this); + connect(minimizeAction, &QAction::triggered, this, &Tray::hideWindow); + + restoreAction = new QAction(tr("&Restore"), this); + connect(restoreAction, &QAction::triggered, this, &Tray::restoreWindow); + + quitAction = new QAction(tr("&Quit"), this); + connect(quitAction, &QAction::triggered, qApp, &QCoreApplication::quit); + + startAction = new QAction(tr("Start"), this); + connect(startAction, &QAction::triggered, this, &Tray::start); + + pauseAction = new QAction(tr("Pause"), this); + connect(pauseAction, &QAction::triggered, this, &Tray::pauseOrUnpause); + + stopAction = new QAction(tr("Stop"), this); + connect(stopAction, &QAction::triggered, this, &Tray::stop); + + statusAction = new QAction(tr("Status:"), this); + statusAction->setEnabled(false); + + trayIconMenu->addAction(statusAction); + trayIconMenu->addSeparator(); + trayIconMenu->addAction(startAction); + trayIconMenu->addAction(pauseAction); + trayIconMenu->addAction(stopAction); + trayIconMenu->addSeparator(); + trayIconMenu->addAction(minimizeAction); + trayIconMenu->addAction(restoreAction); + trayIconMenu->addSeparator(); + trayIconMenu->addAction(quitAction); + + trayIcon->setContextMenu(trayIconMenu); + trayIcon->setIcon(m_icon); + trayIcon->show(); +} + +void Tray::iconActivated(QSystemTrayIcon::ActivationReason reason) +{ + switch (reason) { + case QSystemTrayIcon::Trigger: + case QSystemTrayIcon::DoubleClick: + emit restoreWindow(); + break; + default:; + } +} + +void Tray::updateStatus(Cluster cluster) +{ + QString status = cluster.status(); + if (status.isEmpty()) { + status = "Stopped"; + } + statusAction->setText("Status: " + status); +} + +bool Tray::isVisible() +{ + return trayIcon->isVisible(); +} + +void Tray::setVisible(bool visible) +{ + minimizeAction->setEnabled(visible); + restoreAction->setEnabled(!visible); +} + +static QString getPauseLabel(bool isPaused) +{ + if (isPaused) { + return "Unpause"; + } + return "Pause"; +} + +static QString getStartLabel(bool isRunning) +{ + if (isRunning) { + return "Reload"; + } + return "Start"; +} + +void Tray::updateTrayActions(Cluster cluster) +{ + bool isRunning = cluster.status() == "Running"; + bool isPaused = cluster.status() == "Paused"; + pauseAction->setEnabled(isRunning || isPaused); + stopAction->setEnabled(isRunning || isPaused); + pauseAction->setText(getPauseLabel(isPaused)); + startAction->setText(getStartLabel(isRunning)); +} + +void Tray::disableActions() +{ + startAction->setEnabled(false); + stopAction->setEnabled(false); + pauseAction->setEnabled(false); +} diff --git a/gui/tray.h b/gui/tray.h new file mode 100644 index 0000000000..afa3788df6 --- /dev/null +++ b/gui/tray.h @@ -0,0 +1,45 @@ +#ifndef TRAY_H +#define TRAY_H + +#include "cluster.h" + +#include +#include + +class Tray : public QObject +{ + Q_OBJECT + +public: + explicit Tray(QIcon icon); + bool isVisible(); + void setVisible(bool visible); + void updateStatus(Cluster cluster); + void updateTrayActions(Cluster cluster); + void disableActions(); + +signals: + void restoreWindow(); + void showWindow(); + void hideWindow(); + void start(); + void stop(); + void pauseOrUnpause(); + +private: + void createTrayIcon(); + void createActions(); + void iconActivated(QSystemTrayIcon::ActivationReason reason); + QAction *minimizeAction; + QAction *restoreAction; + QAction *quitAction; + QAction *startAction; + QAction *pauseAction; + QAction *stopAction; + QAction *statusAction; + QSystemTrayIcon *trayIcon; + QMenu *trayIconMenu; + QIcon m_icon; +}; + +#endif // TRAY_H diff --git a/gui/updater.cpp b/gui/updater.cpp new file mode 100644 index 0000000000..ea18e2b279 --- /dev/null +++ b/gui/updater.cpp @@ -0,0 +1,117 @@ +#include "updater.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +Updater::Updater(QVersionNumber version, QIcon icon) +{ + m_version = version; + m_icon = icon; +} + +static bool checkedForUpdateRecently() +{ + QString filePath = QStandardPaths::locate(QStandardPaths::HomeLocation, "/.minikube-gui/last_update_check"); + if (filePath == "") { + return false; + } + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + return false; + } + QTextStream in(&file); + QString line = in.readLine(); + QDateTime nextCheck = QDateTime::fromString(line).addSecs(60*60*24); + QDateTime now = QDateTime::currentDateTime(); + return nextCheck > now; +} + +static void logUpdateCheck() +{ + QDir dir = QDir(QDir::homePath() + "/.minikube-gui"); + if (!dir.exists()) { + dir.mkpath("."); + } + QString filePath = dir.filePath("last_update_check"); + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly)) { + return; + } + QTextStream stream(&file); + stream << QDateTime::currentDateTime().toString() << Qt::endl; +} + +void Updater::checkForUpdates() +{ + if (checkedForUpdateRecently()) { + return; + } + logUpdateCheck(); + QString releases = getRequest("https://storage.googleapis.com/minikube-gui/releases.json"); + QJsonObject latestRelease = + QJsonDocument::fromJson(releases.toUtf8()).array().first().toObject(); + QString latestReleaseVersion = latestRelease["name"].toString(); + QVersionNumber latestReleaseVersionNumber = QVersionNumber::fromString(latestReleaseVersion); + if (m_version >= latestReleaseVersionNumber) { + return; + } + QJsonObject links = latestRelease["links"].toObject(); + QString key; +#if __linux__ + key = "linux"; +#elif __APPLE__ + key = "darwin"; +#else + key = "windows"; +#endif + QString link = links[key].toString(); + notifyUpdate(latestReleaseVersion, link); +} + +void Updater::notifyUpdate(QString latest, QString link) +{ + QDialog dialog; + dialog.setWindowTitle(tr("minikube GUI Update Available")); + dialog.setWindowIcon(m_icon); + dialog.setModal(true); + QFormLayout form(&dialog); + QLabel *msgLabel = new QLabel(); + msgLabel->setText("Version " + latest + + " of minikube GUI is now available!\n\nDownload the update from:"); + form.addWidget(msgLabel); + QLabel *linkLabel = new QLabel(); + linkLabel->setOpenExternalLinks(true); + linkLabel->setText("" + link + ""); + form.addWidget(linkLabel); + QDialogButtonBox buttonBox(Qt::Horizontal, &dialog); + buttonBox.addButton(QString(tr("OK")), QDialogButtonBox::AcceptRole); + connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + form.addRow(&buttonBox); + dialog.exec(); +} + +QString Updater::getRequest(QString url) +{ + QNetworkAccessManager *manager = new QNetworkAccessManager(); + QObject::connect(manager, &QNetworkAccessManager::finished, this, [=](QNetworkReply *reply) { + if (reply->error()) { + qDebug() << reply->errorString(); + } + }); + QNetworkReply *resp = manager->get(QNetworkRequest(QUrl(url))); + QEventLoop loop; + connect(resp, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + return resp->readAll(); +} diff --git a/gui/updater.h b/gui/updater.h new file mode 100644 index 0000000000..2836a63ee9 --- /dev/null +++ b/gui/updater.h @@ -0,0 +1,23 @@ +#ifndef UPDATER_H +#define UPDATER_H + +#include +#include +#include + +class Updater : public QObject +{ + Q_OBJECT + +public: + explicit Updater(QVersionNumber version, QIcon icon); + void checkForUpdates(); + +private: + void notifyUpdate(QString latest, QString link); + QString getRequest(QString url); + QVersionNumber m_version; + QIcon m_icon; +}; + +#endif // UPDATER_H diff --git a/gui/window.cpp b/gui/window.cpp index 449b977235..4f54c63e44 100644 --- a/gui/window.cpp +++ b/gui/window.cpp @@ -97,109 +97,32 @@ Window::Window() { trayIconIcon = new QIcon(":/images/minikube.png"); checkForMinikube(); - isBasicView = true; stackedWidget = new QStackedWidget; - QVBoxLayout *layout = new QVBoxLayout; - dashboardProcess = 0; + commandRunner = new CommandRunner(this); + basicView = new BasicView(); + advancedView = new AdvancedView(*trayIconIcon); + errorMessage = new ErrorMessage(this, *trayIconIcon); + progressWindow = new ProgressWindow(this, *trayIconIcon); + tray = new Tray(*trayIconIcon); + hyperKit = new HyperKit(*trayIconIcon); + updater = new Updater(version, *trayIconIcon); - createClusterGroupBox(); + op = new Operator(advancedView, basicView, commandRunner, errorMessage, progressWindow, tray, hyperKit, updater, stackedWidget, this); - createActions(); - createTrayIcon(); - - createBasicView(); - createAdvancedView(); - trayIcon->show(); - updateButtons(); + stackedWidget->addWidget(basicView->basicView); + stackedWidget->addWidget(advancedView->advancedView); + layout = new QVBoxLayout; layout->addWidget(stackedWidget); setLayout(layout); resize(200, 275); - setWindowTitle(tr("minikube")); setWindowIcon(*trayIconIcon); - checkForUpdates(); -} - -QProcessEnvironment Window::setMacEnv() -{ - QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - QString path = env.value("PATH"); - env.insert("PATH", path + ":/usr/local/bin"); - return env; -} - -void Window::createBasicView() -{ - QWidget *basicView = new QWidget(); - - basicStartButton = new QPushButton(tr("Start")); - basicStopButton = new QPushButton(tr("Stop")); - basicPauseButton = new QPushButton(tr("Pause")); - basicDeleteButton = new QPushButton(tr("Delete")); - basicRefreshButton = new QPushButton(tr("Refresh")); - basicSSHButton = new QPushButton(tr("SSH")); - basicDashboardButton = new QPushButton(tr("Dashboard")); - QPushButton *advancedViewButton = new QPushButton(tr("Advanced View")); - - QVBoxLayout *buttonLayout = new QVBoxLayout; - basicView->setLayout(buttonLayout); - buttonLayout->addWidget(basicStartButton); - buttonLayout->addWidget(basicStopButton); - buttonLayout->addWidget(basicPauseButton); - buttonLayout->addWidget(basicDeleteButton); - buttonLayout->addWidget(basicRefreshButton); - buttonLayout->addWidget(basicSSHButton); - buttonLayout->addWidget(basicDashboardButton); - buttonLayout->addWidget(advancedViewButton); - basicView->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); - stackedWidget->addWidget(basicView); - - connect(basicSSHButton, &QAbstractButton::clicked, this, &Window::sshConsole); - connect(basicDashboardButton, &QAbstractButton::clicked, this, &Window::dashboardBrowser); - connect(basicStartButton, &QAbstractButton::clicked, this, &Window::startSelectedMinikube); - connect(basicStopButton, &QAbstractButton::clicked, this, &Window::stopMinikube); - connect(basicPauseButton, &QAbstractButton::clicked, this, &Window::pauseOrUnpauseMinikube); - connect(basicDeleteButton, &QAbstractButton::clicked, this, &Window::deleteMinikube); - connect(basicRefreshButton, &QAbstractButton::clicked, this, &Window::updateClustersTable); - connect(advancedViewButton, &QAbstractButton::clicked, this, &Window::toAdvancedView); -} - -void Window::toAdvancedView() -{ - isBasicView = false; - stackedWidget->setCurrentIndex(1); - resize(670, 400); - updateButtons(); -} - -void Window::toBasicView() -{ - isBasicView = true; - stackedWidget->setCurrentIndex(0); - resize(200, 275); - updateButtons(); -} - -void Window::createAdvancedView() -{ - connect(sshButton, &QAbstractButton::clicked, this, &Window::sshConsole); - connect(dashboardButton, &QAbstractButton::clicked, this, &Window::dashboardBrowser); - connect(startButton, &QAbstractButton::clicked, this, &Window::startSelectedMinikube); - connect(stopButton, &QAbstractButton::clicked, this, &Window::stopMinikube); - connect(pauseButton, &QAbstractButton::clicked, this, &Window::pauseOrUnpauseMinikube); - connect(deleteButton, &QAbstractButton::clicked, this, &Window::deleteMinikube); - connect(refreshButton, &QAbstractButton::clicked, this, &Window::updateClustersTable); - connect(createButton, &QAbstractButton::clicked, this, &Window::initMachine); - - advancedView->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); - stackedWidget->addWidget(advancedView); } void Window::setVisible(bool visible) { - minimizeAction->setEnabled(visible); - restoreAction->setEnabled(!visible); + tray->setVisible(visible); QDialog::setVisible(visible); } @@ -210,7 +133,7 @@ void Window::closeEvent(QCloseEvent *event) return; } #endif - if (trayIcon->isVisible()) { + if (tray->isVisible()) { QMessageBox::information(this, tr("Systray"), tr("The program will keep running in the " "system tray. To terminate the program, " @@ -221,881 +144,14 @@ void Window::closeEvent(QCloseEvent *event) } } -void Window::messageClicked() -{ - QMessageBox::information(0, tr("Systray"), - tr("Sorry, I already gave what help I could.\n" - "Maybe you should try asking a human?")); -} - -void Window::createActions() -{ - minimizeAction = new QAction(tr("Mi&nimize"), this); - connect(minimizeAction, &QAction::triggered, this, &QWidget::hide); - - restoreAction = new QAction(tr("&Restore"), this); - connect(restoreAction, &QAction::triggered, this, &Window::restoreWindow); - - quitAction = new QAction(tr("&Quit"), this); - connect(quitAction, &QAction::triggered, qApp, &QCoreApplication::quit); - - startAction = new QAction(tr("Start"), this); - connect(startAction, &QAction::triggered, this, &Window::startSelectedMinikube); - - pauseAction = new QAction(tr("Pause"), this); - connect(pauseAction, &QAction::triggered, this, &Window::pauseOrUnpauseMinikube); - - stopAction = new QAction(tr("Stop"), this); - connect(stopAction, &QAction::triggered, this, &Window::stopMinikube); - - statusAction = new QAction(tr("Status:"), this); - statusAction->setEnabled(false); -} - -void Window::updateStatus(Cluster cluster) -{ - QString status = cluster.status(); - if (status.isEmpty()) { - status = "Stopped"; - } - statusAction->setText("Status: " + status); -} - -void Window::iconActivated(QSystemTrayIcon::ActivationReason reason) -{ - switch (reason) { - case QSystemTrayIcon::Trigger: - case QSystemTrayIcon::DoubleClick: - Window::restoreWindow(); - break; - default:; - } -} - -void Window::restoreWindow() -{ - bool wasVisible = isVisible(); - QWidget::showNormal(); - activateWindow(); - if (wasVisible) { - return; - } - // without this delay window doesn't render until updateClusters() completes - delay(); - updateClustersTable(); -} - -void Window::delay() -{ - QCoreApplication::processEvents(QEventLoop::AllEvents, 100); -} - static QString minikubePath() { - QString program = QStandardPaths::findExecutable("minikube"); - if (program.isEmpty()) { - QStringList paths = { "/usr/local/bin" }; - program = QStandardPaths::findExecutable("minikube", paths); - } - return program; -} - -void Window::createTrayIcon() -{ - trayIconMenu = new QMenu(this); - trayIconMenu->addAction(statusAction); - trayIconMenu->addSeparator(); - trayIconMenu->addAction(startAction); - trayIconMenu->addAction(pauseAction); - trayIconMenu->addAction(stopAction); - trayIconMenu->addSeparator(); - trayIconMenu->addAction(minimizeAction); - trayIconMenu->addAction(restoreAction); - trayIconMenu->addSeparator(); - trayIconMenu->addAction(quitAction); - - trayIcon = new QSystemTrayIcon(this); - trayIcon->setContextMenu(trayIconMenu); - trayIcon->setIcon(*trayIconIcon); - - connect(trayIcon, &QSystemTrayIcon::activated, this, &Window::iconActivated); -} - -void Window::startMinikube(QStringList moreArgs) -{ - QString text; - QStringList args = { "start", "-o", "json" }; - args << moreArgs; - bool success = sendMinikubeStart(args, text); -#if __APPLE__ - hyperkitPermissionFix(args, text); -#endif - updateClustersTable(); - if (success) { - return; - } - outputFailedStart(text); -} - -void Window::startSelectedMinikube() -{ - QStringList args = { "-p", selectedClusterName() }; - return startMinikube(args); -} - -void Window::stopMinikube() -{ - QStringList args = { "stop", "-p", selectedClusterName() }; - sendMinikubeCommand(args); - updateClustersTable(); -} - -void Window::pauseMinikube() -{ - QStringList args = { "pause", "-p", selectedClusterName() }; - sendMinikubeCommand(args); - updateClustersTable(); -} - -void Window::unpauseMinikube() -{ - QStringList args = { "unpause", "-p", selectedClusterName() }; - sendMinikubeCommand(args); - updateClustersTable(); -} - -void Window::deleteMinikube() -{ - QStringList args = { "delete", "-p", selectedClusterName() }; - sendMinikubeCommand(args); - updateClustersTable(); -} - -void Window::updateClustersTable() -{ - showLoading(); - QString cluster = selectedClusterName(); - updateClusterList(); - clusterModel->setClusters(clusterList); - setSelectedClusterName(cluster); - updateButtons(); - loading->setHidden(true); - clusterListView->setEnabled(true); - hideLoading(); -} - -void Window::showLoading() -{ - clusterListView->setEnabled(false); - loading->setHidden(false); - loading->raise(); - int width = getCenter(loading->width(), clusterListView->width()); - int height = getCenter(loading->height(), clusterListView->height()); - loading->move(width, height); - delay(); -} - -void Window::hideLoading() -{ - loading->setHidden(true); - clusterListView->setEnabled(true); -} - -int Window::getCenter(int widgetSize, int parentSize) -{ - return parentSize / 2 - widgetSize / 2; -} - -void Window::updateClusterList() -{ - ClusterList clusters; - QStringList args = { "profile", "list", "-o", "json" }; - QString text; - sendMinikubeCommand(args, text); - QStringList lines; -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - lines = text.split("\n", Qt::SkipEmptyParts); -#else - lines = text.split("\n", QString::SkipEmptyParts); -#endif - for (int i = 0; i < lines.size(); i++) { - QString line = lines.at(i); - QJsonParseError error; - QJsonDocument json = QJsonDocument::fromJson(line.toUtf8(), &error); - if (json.isNull()) { - qDebug() << error.errorString(); - continue; - } - if (!json.isObject()) { - continue; - } - QJsonObject par = json.object(); - QJsonArray valid = par["valid"].toArray(); - QJsonArray invalid = par["invalid"].toArray(); - for (int i = 0; i < valid.size(); i++) { - QJsonObject obj = valid[i].toObject(); - Cluster cluster = createClusterObject(obj); - clusters << cluster; - } - for (int i = 0; i < invalid.size(); i++) { - QJsonObject obj = invalid[i].toObject(); - Cluster cluster = createClusterObject(obj); - cluster.setStatus("Invalid"); - clusters << cluster; - } - } - clusterList = clusters; -} - -Cluster Window::createClusterObject(QJsonObject obj) -{ - QString name; - if (obj.contains("Name")) { - name = obj["Name"].toString(); - } - Cluster cluster(name); - if (obj.contains("Status")) { - QString status = obj["Status"].toString(); - cluster.setStatus(status); - } - if (!obj.contains("Config")) { - return cluster; - } - QJsonObject config = obj["Config"].toObject(); - if (config.contains("CPUs")) { - int cpus = config["CPUs"].toInt(); - cluster.setCpus(cpus); - } - if (config.contains("Memory")) { - int memory = config["Memory"].toInt(); - cluster.setMemory(memory); - } - if (config.contains("Driver")) { - QString driver = config["Driver"].toString(); - cluster.setDriver(driver); - } - if (!config.contains("KubernetesConfig")) { - return cluster; - } - QJsonObject k8sConfig = config["KubernetesConfig"].toObject(); - if (k8sConfig.contains("ContainerRuntime")) { - QString containerRuntime = k8sConfig["ContainerRuntime"].toString(); - cluster.setContainerRuntime(containerRuntime); - } - if (k8sConfig.contains("KubernetesVersion")) { - QString k8sVersion = k8sConfig["KubernetesVersion"].toString(); - cluster.setK8sVersion(k8sVersion); - } - return cluster; -} - -QString Window::selectedClusterName() -{ - if (isBasicView) { - return "minikube"; - } - QModelIndex index = clusterListView->currentIndex(); - QVariant variant = index.siblingAtColumn(0).data(Qt::DisplayRole); - if (variant.isNull()) { - return QString(); - } - return variant.toString(); -} - -void Window::setSelectedClusterName(QString cluster) -{ - QAbstractItemModel *model = clusterListView->model(); - QModelIndex start = model->index(0, 0); - QModelIndexList index = model->match(start, Qt::DisplayRole, cluster); - if (index.size() == 0) { - return; - } - clusterListView->setCurrentIndex(index[0]); -} - -void Window::createClusterGroupBox() -{ - advancedView = new QWidget(); - - updateClusterList(); - ClusterList clusters = clusterList; - clusterModel = new ClusterModel(clusters); - - clusterListView = new QTableView(); - clusterListView->setModel(clusterModel); - clusterListView->setSelectionMode(QAbstractItemView::SingleSelection); - clusterListView->setSelectionBehavior(QAbstractItemView::SelectRows); - clusterListView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); - clusterListView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); - clusterListView->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); - clusterListView->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents); - clusterListView->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents); - clusterListView->horizontalHeader()->setSectionResizeMode(5, QHeaderView::ResizeToContents); - clusterListView->horizontalHeader()->setSectionResizeMode(6, QHeaderView::ResizeToContents); - setSelectedClusterName("default"); - - connect(clusterListView, SIGNAL(clicked(QModelIndex)), this, SLOT(updateButtons())); - - startButton = new QPushButton(tr("Start")); - stopButton = new QPushButton(tr("Stop")); - pauseButton = new QPushButton(tr("Pause")); - deleteButton = new QPushButton(tr("Delete")); - refreshButton = new QPushButton(tr("Refresh")); - createButton = new QPushButton(tr("Create")); - sshButton = new QPushButton(tr("SSH")); - dashboardButton = new QPushButton(tr("Dashboard")); - QPushButton *basicViewButton = new QPushButton(tr("Basic View")); - connect(basicViewButton, &QAbstractButton::clicked, this, &Window::toBasicView); - - QHBoxLayout *topButtonLayout = new QHBoxLayout; - topButtonLayout->addWidget(createButton); - topButtonLayout->addWidget(refreshButton); - topButtonLayout->addWidget(basicViewButton); - topButtonLayout->addSpacing(340); - - QHBoxLayout *bottomButtonLayout = new QHBoxLayout; - bottomButtonLayout->addWidget(startButton); - bottomButtonLayout->addWidget(stopButton); - bottomButtonLayout->addWidget(pauseButton); - bottomButtonLayout->addWidget(deleteButton); - bottomButtonLayout->addWidget(sshButton); - bottomButtonLayout->addWidget(dashboardButton); - - QVBoxLayout *clusterLayout = new QVBoxLayout; - clusterLayout->addLayout(topButtonLayout); - clusterLayout->addWidget(clusterListView); - clusterLayout->addLayout(bottomButtonLayout); - advancedView->setLayout(clusterLayout); - - QFont *loadingFont = new QFont(); - loadingFont->setPointSize(30); - loading = new QLabel("Loading..."); - loading->setFont(*loadingFont); - loading->setParent(clusterListView); - loading->setHidden(true); -} - -void Window::updateButtons() -{ - Cluster cluster = selectedCluster(); - if (isBasicView) { - updateBasicButtons(cluster); - } else { - updateAdvancedButtons(cluster); - } - updateTrayActions(cluster); - updateStatus(cluster); -} - -void Window::updateTrayActions(Cluster cluster) -{ - bool isRunning = cluster.status() == "Running"; - bool isPaused = cluster.status() == "Paused"; - pauseAction->setEnabled(isRunning || isPaused); - stopAction->setEnabled(isRunning || isPaused); - pauseAction->setText(getPauseLabel(isRunning)); - startAction->setText(getStartLabel(isRunning)); -} - -Cluster Window::selectedCluster() -{ - QString clusterName = selectedClusterName(); - if (clusterName.isEmpty()) { - return Cluster(); - } - ClusterList clusters = clusterList; - ClusterHash clusterHash; - for (int i = 0; i < clusters.size(); i++) { - Cluster cluster = clusters.at(i); - clusterHash[cluster.name()] = cluster; - } - return clusterHash[clusterName]; -} - -void Window::updateBasicButtons(Cluster cluster) -{ - bool exists = !cluster.isEmpty(); - bool isRunning = cluster.status() == "Running"; - bool isPaused = cluster.status() == "Paused"; - basicStopButton->setEnabled(isRunning || isPaused); - basicPauseButton->setEnabled(isRunning || isPaused); - basicDeleteButton->setEnabled(exists); - basicDashboardButton->setEnabled(isRunning); -#if __linux__ || __APPLE__ - basicSSHButton->setEnabled(exists); -#else - basicSSHButton->setEnabled(false); -#endif - basicPauseButton->setText(getPauseLabel(isPaused)); - basicStartButton->setText(getStartLabel(isRunning)); -} - -QString Window::getPauseLabel(bool isPaused) -{ - if (isPaused) { - return tr("Unpause"); - } - return tr("Pause"); -} - -QString Window::getStartLabel(bool isRunning) -{ - if (isRunning) { - return tr("Reload"); - } - return tr("Start"); -} - -void Window::pauseOrUnpauseMinikube() -{ - Cluster cluster = selectedCluster(); - if (cluster.status() == "Paused") { - unpauseMinikube(); - return; - } - pauseMinikube(); -} - -void Window::updateAdvancedButtons(Cluster cluster) -{ - bool exists = !cluster.isEmpty(); - bool isRunning = cluster.status() == "Running"; - bool isPaused = cluster.status() == "Paused"; - startButton->setEnabled(exists); - stopButton->setEnabled(isRunning || isPaused); - pauseButton->setEnabled(isRunning || isPaused); - deleteButton->setEnabled(exists); - dashboardButton->setEnabled(isRunning); -#if __linux__ || __APPLE__ - sshButton->setEnabled(exists); -#else - sshButton->setEnabled(false); -#endif - pauseButton->setText(getPauseLabel(isPaused)); - startButton->setText(getStartLabel(isRunning)); -} - -bool Window::sendMinikubeCommand(QStringList cmds) -{ - QString text; - return sendMinikubeCommand(cmds, text); -} - -bool Window::sendMinikubeCommand(QStringList cmds, QString &text) -{ - QString program = minikubePath(); - if (program.isEmpty()) { - return false; - } - QStringList arguments = { "--user", "minikube-gui" }; - arguments << cmds; - - QProcess *process = new QProcess(this); -#if __APPLE__ - if (env.isEmpty()) { - env = setMacEnv(); - } - process->setProcessEnvironment(env); -#endif - process->start(program, arguments); - this->setCursor(Qt::WaitCursor); - bool timedOut = process->waitForFinished(300 * 1000); - int exitCode = process->exitCode(); - bool success = !timedOut && exitCode == 0; - this->unsetCursor(); - - text = process->readAllStandardOutput(); - if (success) { - } else { - qDebug() << text; - qDebug() << process->readAllStandardError(); - } - delete process; - return success; -} - -bool Window::sendMinikubeStart(QStringList cmds, QString &text) -{ - QString program = minikubePath(); - if (program.isEmpty()) { - return false; - } - QStringList arguments = { "--user", "minikube-gui" }; - arguments << cmds; - - QProcess *process = new QProcess(this); - connect(process, &QProcess::readyReadStandardOutput, - [process, this]() { startStep(process->readAllStandardOutput()); }); - startProcess = process; -#if __APPLE__ - if (env.isEmpty()) { - env = setMacEnv(); - } - startProcess->setProcessEnvironment(env); -#endif - this->setCursor(Qt::WaitCursor); - startProcess->start(program, arguments); - startProgress(); - while (startProcess->state() != QProcess::NotRunning) { - delay(); - } - endProgress(); - int exitCode = startProcess->exitCode(); - bool success = exitCode == 0; - this->unsetCursor(); - - text = startProcess->readAllStandardOutput(); - if (success) { - } else { - qDebug() << text; - qDebug() << startProcess->readAllStandardError(); - } - delete startProcess; - return success; -} - -void Window::startProgress() -{ - progressDialog = new QDialog(this); - progressDialog->resize(300, 150); - progressDialog->setWindowTitle(tr("minikube start Progress")); - progressDialog->setWindowIcon(*trayIconIcon); - progressDialog->setWindowFlags(Qt::FramelessWindowHint); - progressDialog->setModal(true); - - QVBoxLayout form(progressDialog); - progressText = new QLabel(); - progressText->setText("Starting..."); - progressText->setWordWrap(true); - form.addWidget(progressText); - progressBar.setMaximum(19); - form.addWidget(&progressBar); - QPushButton *cancel = new QPushButton(tr("Cancel")); - connect(cancel, &QAbstractButton::clicked, startProcess, &QProcess::kill); - form.addWidget(cancel); - - progressDialog->open(); -} - -void Window::endProgress() -{ - progressDialog->hide(); - progressBar.setValue(0); -} - -void Window::startStep(QString step) -{ - QStringList lines; -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - lines = step.split("\n", Qt::SkipEmptyParts); -#else - lines = step.split("\n", QString::SkipEmptyParts); -#endif - for (int i = 0; i < lines.size(); i++) { - QJsonDocument json = QJsonDocument::fromJson(lines[i].toUtf8()); - QJsonObject object = json.object(); - QString type = object["type"].toString(); - if (type != "io.k8s.sigs.minikube.step") { - return; - } - QJsonObject data = object["data"].toObject(); - QString stringStep = data["currentstep"].toString(); - int currStep = stringStep.toInt(); - QString message = data["message"].toString(); - progressBar.setValue(currStep); - progressText->setText(message); - } -} - -static QString profile = "minikube"; -static int cpus = 2; -static int memory = 2400; -static QString driver = ""; -static QString containerRuntime = ""; -static QString k8sVersion = ""; - -void Window::askName() -{ - QDialog dialog; - dialog.setWindowTitle(tr("Create minikube Cluster")); - dialog.setWindowIcon(*trayIconIcon); - dialog.setModal(true); - - QFormLayout form(&dialog); - QDialogButtonBox buttonBox(Qt::Horizontal, &dialog); - QLineEdit profileField(profile, &dialog); - form.addRow(new QLabel(tr("Profile")), &profileField); - buttonBox.addButton(QString(tr("Use Default Values")), QDialogButtonBox::AcceptRole); - connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); - buttonBox.addButton(QString(tr("Set Custom Values")), QDialogButtonBox::RejectRole); - connect(&buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - form.addRow(&buttonBox); - - int code = dialog.exec(); - profile = profileField.text(); - if (code == QDialog::Accepted) { - QStringList args = { "-p", profile }; - startMinikube(args); - } else if (code == QDialog::Rejected) { - askCustom(); - } -} - -void Window::askCustom() -{ - QDialog dialog; - dialog.setWindowTitle(tr("Set Cluster Values")); - dialog.setWindowIcon(*trayIconIcon); - dialog.setModal(true); - - QFormLayout form(&dialog); - driverComboBox = new QComboBox; - driverComboBox->addItems({ "docker", "virtualbox", "vmware", "podman" }); -#if __linux__ - driverComboBox->addItem("kvm2"); -#elif __APPLE__ - driverComboBox->addItems({ "hyperkit", "parallels" }); -#else - driverComboBox->addItem("hyperv"); -#endif - form.addRow(new QLabel(tr("Driver")), driverComboBox); - containerRuntimeComboBox = new QComboBox; - containerRuntimeComboBox->addItems({ "docker", "containerd", "crio" }); - form.addRow(new QLabel(tr("Container Runtime")), containerRuntimeComboBox); - k8sVersionComboBox = new QComboBox; - k8sVersionComboBox->addItems({ "stable", "latest", "none" }); - form.addRow(new QLabel(tr("Kubernetes Version")), k8sVersionComboBox); - QLineEdit cpuField(QString::number(cpus), &dialog); - form.addRow(new QLabel(tr("CPUs")), &cpuField); - QLineEdit memoryField(QString::number(memory), &dialog); - form.addRow(new QLabel(tr("Memory")), &memoryField); - - QDialogButtonBox buttonBox(Qt::Horizontal, &dialog); - buttonBox.addButton(QString(tr("Create")), QDialogButtonBox::AcceptRole); - connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); - buttonBox.addButton(QString(tr("Cancel")), QDialogButtonBox::RejectRole); - connect(&buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - form.addRow(&buttonBox); - - int code = dialog.exec(); - if (code == QDialog::Accepted) { - driver = driverComboBox->itemText(driverComboBox->currentIndex()); - containerRuntime = - containerRuntimeComboBox->itemText(containerRuntimeComboBox->currentIndex()); - k8sVersion = k8sVersionComboBox->itemText(k8sVersionComboBox->currentIndex()); - if (k8sVersion == "none") { - k8sVersion = "v0.0.0"; - } - cpus = cpuField.text().toInt(); - memory = memoryField.text().toInt(); - QStringList args = { "-p", - profile, - "--driver", - driver, - "--container-runtime", - containerRuntime, - "--kubernetes-version", - k8sVersion, - "--cpus", - QString::number(cpus), - "--memory", - QString::number(memory) }; - startMinikube(args); - } -} - -void Window::outputFailedStart(QString text) -{ - QStringList lines; -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - lines = text.split("\n", Qt::SkipEmptyParts); -#else - lines = text.split("\n", QString::SkipEmptyParts); -#endif - for (int i = 0; i < lines.size(); i++) { - QString line = lines.at(i); - QJsonParseError error; - QJsonDocument json = QJsonDocument::fromJson(line.toUtf8(), &error); - if (json.isNull() || !json.isObject()) { - continue; - } - QJsonObject par = json.object(); - QJsonObject data = par["data"].toObject(); - if (!data.contains("exitcode")) { - continue; - } - QString advice = data["advice"].toString(); - QString message = data["message"].toString(); - QString name = data["name"].toString(); - QString url = data["url"].toString(); - QString issues = data["issues"].toString(); - - QDialog dialog; - dialog.setWindowTitle(tr("minikube start failed")); - dialog.setWindowIcon(*trayIconIcon); - dialog.setFixedWidth(600); - dialog.setModal(true); - QFormLayout form(&dialog); - createLabel("Error Code", name, &form, false); - createLabel("Advice", advice, &form, false); - QTextEdit *errorMessage = new QTextEdit(); - errorMessage->setText(message); - errorMessage->setWordWrapMode(QTextOption::WrapAnywhere); - int pointSize = errorMessage->font().pointSize(); - errorMessage->setFont(QFont("Courier", pointSize)); - errorMessage->setAutoFillBackground(true); - errorMessage->setReadOnly(true); - form.addRow(errorMessage); - createLabel("Link to documentation", url, &form, true); - createLabel("Link to related issue", issues, &form, true); - QLabel *fileLabel = new QLabel(this); - fileLabel->setOpenExternalLinks(true); - fileLabel->setWordWrap(true); - QString logFile = QDir::homePath() + "/.minikube/logs/lastStart.txt"; - fileLabel->setText("View log file"); - form.addRow(fileLabel); - QDialogButtonBox buttonBox(Qt::Horizontal, &dialog); - buttonBox.addButton(QString(tr("OK")), QDialogButtonBox::AcceptRole); - connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); - form.addRow(&buttonBox); - dialog.exec(); - } -} - -QLabel *Window::createLabel(QString title, QString text, QFormLayout *form, bool isLink) -{ - QLabel *label = new QLabel(this); - if (!text.isEmpty()) { - form->addRow(label); - } - if (isLink) { - label->setOpenExternalLinks(true); - text = "" + text + ""; - } - label->setWordWrap(true); - label->setText(title + ": " + text); - return label; -} - -void Window::initMachine() -{ - askName(); - updateClustersTable(); -} - -void Window::sshConsole() -{ - QString program = minikubePath(); -#ifndef QT_NO_TERMWIDGET - QMainWindow *mainWindow = new QMainWindow(); - int startnow = 0; // set shell program first - - QTermWidget *console = new QTermWidget(startnow); - - QFont font = QApplication::font(); - font.setFamily("Monospace"); - font.setPointSize(10); - - console->setTerminalFont(font); - console->setColorScheme("Tango"); - console->setShellProgram(program); - QStringList args = { "ssh", "-p", selectedClusterName() }; - console->setArgs(args); - console->startShellProgram(); - - QObject::connect(console, SIGNAL(finished()), mainWindow, SLOT(close())); - - mainWindow->setWindowTitle(nameLabel->text()); - mainWindow->resize(800, 400); - mainWindow->setCentralWidget(console); - mainWindow->show(); -#elif __APPLE__ - QString command = program + " ssh -p " + selectedClusterName(); - QStringList arguments = { "-e", "tell app \"Terminal\"", "-e", "activate", - "-e", "do script \"" + command + "\"", "-e", "end tell" }; - QProcess *process = new QProcess(this); - process->start("/usr/bin/osascript", arguments); -#else - QString terminal = qEnvironmentVariable("TERMINAL"); - if (terminal.isEmpty()) { - terminal = "x-terminal-emulator"; - if (QStandardPaths::findExecutable(terminal).isEmpty()) { - terminal = "xterm"; - } - } - - QStringList arguments = { "-e", QString("%1 ssh -p %2").arg(program, selectedClusterName()) }; - QProcess *process = new QProcess(this); - process->start(QStandardPaths::findExecutable(terminal), arguments); -#endif -} - -#if __APPLE__ -bool Window::hyperkitPermissionFix(QStringList args, QString text) -{ - if (!text.contains("docker-machine-driver-hyperkit needs to run with elevated permissions")) { - return false; - } - if (!showHyperKitMessage()) { - return false; - } - - hyperkitPermission(); - return sendMinikubeCommand(args, text); -} - -void Window::hyperkitPermission() -{ - QString command = "sudo chown root:wheel ~/.minikube/bin/docker-machine-driver-hyperkit && " - "sudo chmod u+s ~/.minikube/bin/docker-machine-driver-hyperkit && exit"; - QStringList arguments = { "-e", "tell app \"Terminal\"", - "-e", "set w to do script \"" + command + "\"", - "-e", "activate", - "-e", "repeat", - "-e", "delay 0.1", - "-e", "if not busy of w then exit repeat", - "-e", "end repeat", - "-e", "end tell" }; - QProcess *process = new QProcess(this); - process->start("/usr/bin/osascript", arguments); - process->waitForFinished(-1); -} - -bool Window::showHyperKitMessage() -{ - QMessageBox msgBox; - msgBox.setWindowTitle(tr("HyperKit Permissions Required")); - msgBox.setWindowIcon(*trayIconIcon); - msgBox.setModal(true); - msgBox.setText(tr("The HyperKit driver requires a one-time sudo permission.\n\nIf you'd like " - "to proceed, press OK and then enter your password into the terminal prompt, " - "the start will resume after.")); - msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); - msgBox.setDefaultButton(QMessageBox::Ok); - int code = msgBox.exec(); - return code == QMessageBox::Ok; -} -#endif - -void Window::dashboardBrowser() -{ - dashboardClose(); - - QString program = minikubePath(); - QProcess *process = new QProcess(this); - QStringList arguments = { "dashboard", "-p", selectedClusterName() }; - process->start(program, arguments); - - dashboardProcess = process; - dashboardProcess->waitForStarted(); -} - -void Window::dashboardClose() -{ - if (dashboardProcess) { - dashboardProcess->terminate(); - dashboardProcess->waitForFinished(); + QString minikubePath = QStandardPaths::findExecutable("minikube"); + if (!minikubePath.isEmpty()) { + return minikubePath; } + QStringList path = { "/usr/local/bin" }; + return QStandardPaths::findExecutable("minikube", path); } void Window::checkForMinikube() @@ -1128,64 +184,4 @@ void Window::checkForMinikube() exit(EXIT_FAILURE); } -void Window::checkForUpdates() -{ - QString releases = getRequest("https://storage.googleapis.com/minikube-gui/releases.json"); - QJsonObject latestRelease = - QJsonDocument::fromJson(releases.toUtf8()).array().first().toObject(); - QString latestReleaseVersion = latestRelease["name"].toString(); - QVersionNumber latestReleaseVersionNumber = QVersionNumber::fromString(latestReleaseVersion); - if (version >= latestReleaseVersionNumber) { - return; - } - QJsonObject links = latestRelease["links"].toObject(); - QString key; -#if __linux__ - key = "linux"; -#elif __APPLE__ - key = "darwin"; -#else - key = "windows"; -#endif - QString link = links[key].toString(); - notifyUpdate(latestReleaseVersion, link); -} - -void Window::notifyUpdate(QString latest, QString link) -{ - QDialog dialog; - dialog.setWindowTitle(tr("minikube GUI Update Available")); - dialog.setWindowIcon(*trayIconIcon); - dialog.setModal(true); - QFormLayout form(&dialog); - QLabel *msgLabel = new QLabel(this); - msgLabel->setText("Version " + latest - + " of minikube GUI is now available!\n\nDownload the update from:"); - form.addWidget(msgLabel); - QLabel *linkLabel = new QLabel(this); - linkLabel->setOpenExternalLinks(true); - linkLabel->setText("" + link + ""); - form.addWidget(linkLabel); - QDialogButtonBox buttonBox(Qt::Horizontal, &dialog); - buttonBox.addButton(QString(tr("OK")), QDialogButtonBox::AcceptRole); - connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); - form.addRow(&buttonBox); - dialog.exec(); -} - -QString Window::getRequest(QString url) -{ - QNetworkAccessManager *manager = new QNetworkAccessManager(); - QObject::connect(manager, &QNetworkAccessManager::finished, this, [=](QNetworkReply *reply) { - if (reply->error()) { - qDebug() << reply->errorString(); - } - }); - QNetworkReply *resp = manager->get(QNetworkRequest(QUrl(url))); - QEventLoop loop; - connect(resp, &QNetworkReply::finished, &loop, &QEventLoop::quit); - loop.exec(); - return resp->readAll(); -} - #endif diff --git a/gui/window.h b/gui/window.h index dba9f1f95d..a78944a6b8 100644 --- a/gui/window.h +++ b/gui/window.h @@ -84,6 +84,14 @@ class QProcess; QT_END_NAMESPACE #include "cluster.h" +#include "basicview.h" +#include "advancedview.h" +#include "progresswindow.h" +#include "operator.h" +#include "errormessage.h" +#include "tray.h" +#include "hyperkit.h" +#include "updater.h" class Window : public QDialog { @@ -93,128 +101,31 @@ public: Window(); void setVisible(bool visible) override; + void restoreWindow(); protected: void closeEvent(QCloseEvent *event) override; -private slots: - void messageClicked(); - void updateButtons(); - void dashboardClose(); - private: - // Tray icon - void createTrayIcon(); - void createActions(); - void updateStatus(Cluster cluster); - void updateTrayActions(Cluster cluster); - void iconActivated(QSystemTrayIcon::ActivationReason reason); - QAction *minimizeAction; - QAction *restoreAction; - QAction *quitAction; - QAction *startAction; - QAction *pauseAction; - QAction *stopAction; - QAction *statusAction; - QSystemTrayIcon *trayIcon; - QMenu *trayIconMenu; QIcon *trayIconIcon; - // Basic view - void createBasicView(); - void toBasicView(); - void updateBasicButtons(Cluster cluster); - QPushButton *basicStartButton; - QPushButton *basicStopButton; - QPushButton *basicPauseButton; - QPushButton *basicDeleteButton; - QPushButton *basicRefreshButton; - QPushButton *basicSSHButton; - QPushButton *basicDashboardButton; - - // Advanced view - void createAdvancedView(); - void toAdvancedView(); - void createClusterGroupBox(); - void updateAdvancedButtons(Cluster cluster); - QPushButton *startButton; - QPushButton *stopButton; - QPushButton *pauseButton; - QPushButton *deleteButton; - QPushButton *refreshButton; - QPushButton *createButton; - QPushButton *sshButton; - QPushButton *dashboardButton; - QWidget *advancedView; - - // Cluster table - QString selectedClusterName(); - void setSelectedClusterName(QString cluster); - Cluster selectedCluster(); - void updateClusterList(); - void updateClustersTable(); - void showLoading(); - void hideLoading(); - ClusterModel *clusterModel; - QTableView *clusterListView; - ClusterList clusterList; - QLabel *loading; - - // Create cluster - void askCustom(); - void askName(); - QComboBox *driverComboBox; - QComboBox *containerRuntimeComboBox; - QComboBox *k8sVersionComboBox; - - // Start Commands - void startMinikube(QStringList args); - void startSelectedMinikube(); - bool sendMinikubeStart(QStringList cmds, QString &text); - void startProgress(); - void endProgress(); - void startStep(QString step); - QDialog *progressDialog; - QProgressBar progressBar; - QLabel *progressText; - QProcess *startProcess; - - // Other Commands - void stopMinikube(); - void pauseMinikube(); - void unpauseMinikube(); - void pauseOrUnpauseMinikube(); - void deleteMinikube(); - bool sendMinikubeCommand(QStringList cmds); - bool sendMinikubeCommand(QStringList cmds, QString &text); - void initMachine(); - void sshConsole(); - void dashboardBrowser(); - Cluster createClusterObject(QJsonObject obj); - QProcess *dashboardProcess; - QProcessEnvironment env; -#if __APPLE__ - void hyperkitPermission(); - bool hyperkitPermissionFix(QStringList args, QString text); - bool showHyperKitMessage(); -#endif - - // Error messaging - void outputFailedStart(QString text); - QLabel *createLabel(QString title, QString text, QFormLayout *form, bool isLink); - void checkForMinikube(); - void restoreWindow(); - QString getPauseLabel(bool isPaused); - QString getStartLabel(bool isRunning); - QProcessEnvironment setMacEnv(); QStackedWidget *stackedWidget; - bool isBasicView; - void delay(); - int getCenter(int widgetSize, int parentSize); void checkForUpdates(); QString getRequest(QString url); void notifyUpdate(QString latest, QString link); + + BasicView *basicView; + AdvancedView *advancedView; + Operator *op; + CommandRunner *commandRunner; + ErrorMessage *errorMessage; + ProgressWindow *progressWindow; + Tray *tray; + HyperKit *hyperKit; + Updater *updater; + QVBoxLayout *layout; + }; #endif // QT_NO_SYSTEMTRAYICON