From f8a81ebe361e0e2e32beb1df019869785153daed Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Sun, 1 Mar 2026 09:21:53 -0700 Subject: [PATCH] Implement Milestone 2 profile schema, dialog, and connect lifecycle --- CMakeLists.txt | 2 + src/profile_dialog.cpp | 99 +++++++++++++++++ src/profile_dialog.h | 35 ++++++ src/profile_repository.cpp | 219 +++++++++++++++++++++++++++++++------ src/profile_repository.h | 17 ++- src/profiles_window.cpp | 120 ++++++++++++++------ src/profiles_window.h | 7 +- src/session_window.cpp | 74 +++++++++++-- src/session_window.h | 7 +- 9 files changed, 488 insertions(+), 92 deletions(-) create mode 100644 src/profile_dialog.cpp create mode 100644 src/profile_dialog.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a1e1956..e052dd9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,8 @@ qt_standard_project_setup() add_executable(orbithub src/main.cpp + src/profile_dialog.cpp + src/profile_dialog.h src/profile_repository.cpp src/profile_repository.h src/profiles_window.cpp diff --git a/src/profile_dialog.cpp b/src/profile_dialog.cpp new file mode 100644 index 0000000..b07015f --- /dev/null +++ b/src/profile_dialog.cpp @@ -0,0 +1,99 @@ +#include "profile_dialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +ProfileDialog::ProfileDialog(QWidget* parent) + : QDialog(parent), + m_nameInput(new QLineEdit(this)), + m_hostInput(new QLineEdit(this)), + m_portInput(new QSpinBox(this)), + m_usernameInput(new QLineEdit(this)), + m_protocolInput(new QComboBox(this)), + m_authModeInput(new QComboBox(this)) +{ + resize(420, 260); + + auto* layout = new QVBoxLayout(this); + auto* form = new QFormLayout(); + + m_nameInput->setPlaceholderText(QStringLiteral("Production Bastion")); + m_hostInput->setPlaceholderText(QStringLiteral("example.internal")); + m_portInput->setRange(1, 65535); + m_portInput->setValue(22); + m_usernameInput->setPlaceholderText(QStringLiteral("deploy")); + + m_protocolInput->addItems({QStringLiteral("SSH"), QStringLiteral("RDP"), QStringLiteral("VNC")}); + m_authModeInput->addItems({QStringLiteral("Password"), QStringLiteral("Private Key")}); + + form->addRow(QStringLiteral("Name"), m_nameInput); + form->addRow(QStringLiteral("Host"), m_hostInput); + form->addRow(QStringLiteral("Port"), m_portInput); + form->addRow(QStringLiteral("Username"), m_usernameInput); + form->addRow(QStringLiteral("Protocol"), m_protocolInput); + form->addRow(QStringLiteral("Auth Mode"), m_authModeInput); + + auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + + layout->addLayout(form); + layout->addWidget(buttons); +} + +void ProfileDialog::setDialogTitle(const QString& title) +{ + setWindowTitle(title); +} + +void ProfileDialog::setProfile(const Profile& profile) +{ + m_nameInput->setText(profile.name); + m_hostInput->setText(profile.host); + m_portInput->setValue(profile.port > 0 ? profile.port : 22); + m_usernameInput->setText(profile.username); + + const int protocolIndex = m_protocolInput->findText(profile.protocol); + m_protocolInput->setCurrentIndex(protocolIndex >= 0 ? protocolIndex : 0); + + const int authModeIndex = m_authModeInput->findText(profile.authMode); + m_authModeInput->setCurrentIndex(authModeIndex >= 0 ? authModeIndex : 0); +} + +Profile ProfileDialog::profile() const +{ + Profile profile; + profile.id = -1; + profile.name = m_nameInput->text().trimmed(); + profile.host = m_hostInput->text().trimmed(); + profile.port = m_portInput->value(); + profile.username = m_usernameInput->text().trimmed(); + profile.protocol = m_protocolInput->currentText(); + profile.authMode = m_authModeInput->currentText(); + return profile; +} + +void ProfileDialog::accept() +{ + if (m_nameInput->text().trimmed().isEmpty()) { + QMessageBox::warning(this, + QStringLiteral("Validation Error"), + QStringLiteral("Profile name is required.")); + return; + } + + if (m_hostInput->text().trimmed().isEmpty()) { + QMessageBox::warning(this, + QStringLiteral("Validation Error"), + QStringLiteral("Host is required.")); + return; + } + + QDialog::accept(); +} diff --git a/src/profile_dialog.h b/src/profile_dialog.h new file mode 100644 index 0000000..c7830ee --- /dev/null +++ b/src/profile_dialog.h @@ -0,0 +1,35 @@ +#ifndef ORBITHUB_PROFILE_DIALOG_H +#define ORBITHUB_PROFILE_DIALOG_H + +#include "profile_repository.h" + +#include + +class QComboBox; +class QLineEdit; +class QSpinBox; + +class ProfileDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ProfileDialog(QWidget* parent = nullptr); + + void setDialogTitle(const QString& title); + void setProfile(const Profile& profile); + Profile profile() const; + +protected: + void accept() override; + +private: + QLineEdit* m_nameInput; + QLineEdit* m_hostInput; + QSpinBox* m_portInput; + QLineEdit* m_usernameInput; + QComboBox* m_protocolInput; + QComboBox* m_authModeInput; +}; + +#endif diff --git a/src/profile_repository.cpp b/src/profile_repository.cpp index 59884e3..ae761c9 100644 --- a/src/profile_repository.cpp +++ b/src/profile_repository.cpp @@ -1,6 +1,7 @@ #include "profile_repository.h" #include +#include #include #include #include @@ -20,6 +21,35 @@ QString buildDatabasePath() return dataDir.filePath(QStringLiteral("orbithub_profiles.sqlite")); } + +void bindProfileFields(QSqlQuery& query, const Profile& profile) +{ + query.addBindValue(profile.name.trimmed()); + query.addBindValue(profile.host.trimmed()); + query.addBindValue(profile.port); + query.addBindValue(profile.username.trimmed()); + query.addBindValue(profile.protocol.trimmed()); + query.addBindValue(profile.authMode.trimmed()); +} + +Profile profileFromQuery(const QSqlQuery& query) +{ + Profile profile; + profile.id = query.value(0).toLongLong(); + profile.name = query.value(1).toString(); + profile.host = query.value(2).toString(); + profile.port = query.value(3).toInt(); + profile.username = query.value(4).toString(); + profile.protocol = query.value(5).toString(); + profile.authMode = query.value(6).toString(); + return profile; +} + +bool isProfileValid(const Profile& profile) +{ + return !profile.name.trimmed().isEmpty() && !profile.host.trimmed().isEmpty() + && profile.port >= 1 && profile.port <= 65535; +} } ProfileRepository::ProfileRepository() : m_connectionName(QStringLiteral("orbithub_main")) @@ -45,6 +75,11 @@ QString ProfileRepository::initError() const return m_initError; } +QString ProfileRepository::lastError() const +{ + return m_lastError; +} + std::vector ProfileRepository::listProfiles(const QString& searchQuery) const { std::vector result; @@ -53,68 +88,115 @@ std::vector ProfileRepository::listProfiles(const QString& searchQuery) return result; } + setLastError(QString()); + QSqlQuery query(QSqlDatabase::database(m_connectionName)); if (searchQuery.trimmed().isEmpty()) { query.prepare(QStringLiteral( - "SELECT id, name FROM profiles ORDER BY lower(name) ASC, id ASC")); + "SELECT id, name, host, port, username, protocol, auth_mode " + "FROM profiles " + "ORDER BY lower(name) ASC, id ASC")); } else { query.prepare(QStringLiteral( - "SELECT id, name FROM profiles " - "WHERE lower(name) LIKE lower(?) " + "SELECT id, name, host, port, username, protocol, auth_mode " + "FROM profiles " + "WHERE lower(name) LIKE lower(?) OR lower(host) LIKE lower(?) " "ORDER BY lower(name) ASC, id ASC")); - query.addBindValue(QStringLiteral("%") + searchQuery.trimmed() + QStringLiteral("%")); + const QString search = QStringLiteral("%") + searchQuery.trimmed() + QStringLiteral("%"); + query.addBindValue(search); + query.addBindValue(search); } if (!query.exec()) { + setLastError(query.lastError().text()); return result; } while (query.next()) { - result.push_back(Profile{query.value(0).toLongLong(), query.value(1).toString()}); + result.push_back(profileFromQuery(query)); } return result; } -std::optional ProfileRepository::createProfile(const QString& name) const +std::optional ProfileRepository::getProfile(qint64 id) const { if (!QSqlDatabase::contains(m_connectionName)) { return std::nullopt; } - const QString trimmedName = name.trimmed(); - if (trimmedName.isEmpty()) { - return std::nullopt; - } + setLastError(QString()); QSqlQuery query(QSqlDatabase::database(m_connectionName)); - query.prepare(QStringLiteral("INSERT INTO profiles(name) VALUES (?)")); - query.addBindValue(trimmedName); - - if (!query.exec()) { - return std::nullopt; - } - - return Profile{query.lastInsertId().toLongLong(), trimmedName}; -} - -bool ProfileRepository::updateProfile(qint64 id, const QString& name) const -{ - if (!QSqlDatabase::contains(m_connectionName)) { - return false; - } - - const QString trimmedName = name.trimmed(); - if (trimmedName.isEmpty()) { - return false; - } - - QSqlQuery query(QSqlDatabase::database(m_connectionName)); - query.prepare(QStringLiteral("UPDATE profiles SET name = ? WHERE id = ?")); - query.addBindValue(trimmedName); + query.prepare(QStringLiteral( + "SELECT id, name, host, port, username, protocol, auth_mode " + "FROM profiles WHERE id = ?")); query.addBindValue(id); if (!query.exec()) { + setLastError(query.lastError().text()); + return std::nullopt; + } + + if (!query.next()) { + return std::nullopt; + } + + return profileFromQuery(query); +} + +std::optional ProfileRepository::createProfile(const Profile& profile) const +{ + if (!QSqlDatabase::contains(m_connectionName)) { + return std::nullopt; + } + + setLastError(QString()); + + if (!isProfileValid(profile)) { + setLastError(QStringLiteral("Name, host, and a valid port are required.")); + return std::nullopt; + } + + QSqlQuery query(QSqlDatabase::database(m_connectionName)); + query.prepare(QStringLiteral( + "INSERT INTO profiles(name, host, port, username, protocol, auth_mode) " + "VALUES (?, ?, ?, ?, ?, ?)")); + bindProfileFields(query, profile); + + if (!query.exec()) { + setLastError(query.lastError().text()); + return std::nullopt; + } + + Profile created = profile; + created.id = query.lastInsertId().toLongLong(); + return created; +} + +bool ProfileRepository::updateProfile(const Profile& profile) const +{ + if (!QSqlDatabase::contains(m_connectionName)) { + return false; + } + + setLastError(QString()); + + if (profile.id < 0 || !isProfileValid(profile)) { + setLastError(QStringLiteral("Invalid profile data.")); + return false; + } + + QSqlQuery query(QSqlDatabase::database(m_connectionName)); + query.prepare(QStringLiteral( + "UPDATE profiles " + "SET name = ?, host = ?, port = ?, username = ?, protocol = ?, auth_mode = ? " + "WHERE id = ?")); + bindProfileFields(query, profile); + query.addBindValue(profile.id); + + if (!query.exec()) { + setLastError(query.lastError().text()); return false; } @@ -127,11 +209,14 @@ bool ProfileRepository::deleteProfile(qint64 id) const return false; } + setLastError(QString()); + QSqlQuery query(QSqlDatabase::database(m_connectionName)); query.prepare(QStringLiteral("DELETE FROM profiles WHERE id = ?")); query.addBindValue(id); if (!query.exec()) { + setLastError(query.lastError().text()); return false; } @@ -152,12 +237,74 @@ bool ProfileRepository::initializeDatabase() const bool created = query.exec(QStringLiteral( "CREATE TABLE IF NOT EXISTS profiles (" "id INTEGER PRIMARY KEY AUTOINCREMENT," - "name TEXT NOT NULL UNIQUE" + "name TEXT NOT NULL UNIQUE," + "host TEXT NOT NULL DEFAULT ''," + "port INTEGER NOT NULL DEFAULT 22," + "username TEXT NOT NULL DEFAULT ''," + "protocol TEXT NOT NULL DEFAULT 'SSH'," + "auth_mode TEXT NOT NULL DEFAULT 'Password'" ")")); if (!created) { m_initError = query.lastError().text(); + return false; } - return created; + if (!ensureProfileSchema()) { + m_initError = m_lastError; + return false; + } + + return true; +} + +bool ProfileRepository::ensureProfileSchema() const +{ + if (!QSqlDatabase::contains(m_connectionName)) { + setLastError(QStringLiteral("Database connection missing.")); + return false; + } + + QSqlQuery tableInfo(QSqlDatabase::database(m_connectionName)); + if (!tableInfo.exec(QStringLiteral("PRAGMA table_info(profiles)"))) { + setLastError(tableInfo.lastError().text()); + return false; + } + + QSet columns; + while (tableInfo.next()) { + columns.insert(tableInfo.value(1).toString()); + } + + struct ColumnDef { + QString name; + QString ddl; + }; + + const std::vector required = { + {QStringLiteral("host"), QStringLiteral("ALTER TABLE profiles ADD COLUMN host TEXT NOT NULL DEFAULT ''")}, + {QStringLiteral("port"), QStringLiteral("ALTER TABLE profiles ADD COLUMN port INTEGER NOT NULL DEFAULT 22")}, + {QStringLiteral("username"), QStringLiteral("ALTER TABLE profiles ADD COLUMN username TEXT NOT NULL DEFAULT ''")}, + {QStringLiteral("protocol"), QStringLiteral("ALTER TABLE profiles ADD COLUMN protocol TEXT NOT NULL DEFAULT 'SSH'")}, + {QStringLiteral("auth_mode"), QStringLiteral("ALTER TABLE profiles ADD COLUMN auth_mode TEXT NOT NULL DEFAULT 'Password'")}}; + + for (const ColumnDef& column : required) { + if (columns.contains(column.name)) { + continue; + } + + QSqlQuery alter(QSqlDatabase::database(m_connectionName)); + if (!alter.exec(column.ddl)) { + setLastError(alter.lastError().text()); + return false; + } + } + + setLastError(QString()); + return true; +} + +void ProfileRepository::setLastError(const QString& error) const +{ + m_lastError = error; } diff --git a/src/profile_repository.h b/src/profile_repository.h index 7ef5e8f..fef063d 100644 --- a/src/profile_repository.h +++ b/src/profile_repository.h @@ -2,14 +2,20 @@ #define ORBITHUB_PROFILE_REPOSITORY_H #include +#include #include #include struct Profile { - qint64 id; + qint64 id = -1; QString name; + QString host; + int port = 22; + QString username; + QString protocol = QStringLiteral("SSH"); + QString authMode = QStringLiteral("Password"); }; class ProfileRepository @@ -19,17 +25,22 @@ public: ~ProfileRepository(); QString initError() const; + QString lastError() const; std::vector listProfiles(const QString& searchQuery = QString()) const; - std::optional createProfile(const QString& name) const; - bool updateProfile(qint64 id, const QString& name) const; + std::optional getProfile(qint64 id) const; + std::optional createProfile(const Profile& profile) const; + bool updateProfile(const Profile& profile) const; bool deleteProfile(qint64 id) const; private: QString m_connectionName; QString m_initError; + mutable QString m_lastError; bool initializeDatabase(); + bool ensureProfileSchema() const; + void setLastError(const QString& error) const; }; #endif diff --git a/src/profiles_window.cpp b/src/profiles_window.cpp index b00a8f2..5861c1a 100644 --- a/src/profiles_window.cpp +++ b/src/profiles_window.cpp @@ -1,5 +1,6 @@ #include "profiles_window.h" +#include "profile_dialog.h" #include "profile_repository.h" #include "session_window.h" @@ -7,7 +8,6 @@ #include #include -#include #include #include #include @@ -18,6 +18,14 @@ #include #include +namespace { +QString formatProfileListItem(const Profile& profile) +{ + return QStringLiteral("%1 [%2 %3:%4]") + .arg(profile.name, profile.protocol, profile.host, QString::number(profile.port)); +} +} + ProfilesWindow::ProfilesWindow(QWidget* parent) : QMainWindow(parent), m_searchBox(nullptr), @@ -28,7 +36,7 @@ ProfilesWindow::ProfilesWindow(QWidget* parent) m_repository(std::make_unique()) { setWindowTitle(QStringLiteral("OrbitHub Profiles")); - resize(520, 620); + resize(640, 620); setupUi(); @@ -57,7 +65,7 @@ void ProfilesWindow::setupUi() auto* searchLabel = new QLabel(QStringLiteral("Search"), central); m_searchBox = new QLineEdit(central); - m_searchBox->setPlaceholderText(QStringLiteral("Filter profiles...")); + m_searchBox->setPlaceholderText(QStringLiteral("Filter by name or host...")); m_profilesList = new QListWidget(central); m_profilesList->setSelectionMode(QAbstractItemView::SingleSelection); @@ -97,15 +105,31 @@ void ProfilesWindow::setupUi() void ProfilesWindow::loadProfiles(const QString& query) { m_profilesList->clear(); + m_profileCache.clear(); const std::vector profiles = m_repository->listProfiles(query); + if (!m_repository->lastError().isEmpty()) { + QMessageBox::warning(this, + QStringLiteral("Load Profiles"), + QStringLiteral("Failed to load profiles: %1") + .arg(m_repository->lastError())); + return; + } + for (const Profile& profile : profiles) { - auto* item = new QListWidgetItem(profile.name, m_profilesList); + auto* item = new QListWidgetItem(formatProfileListItem(profile), m_profilesList); item->setData(Qt::UserRole, QVariant::fromValue(profile.id)); + item->setToolTip(QStringLiteral("%1://%2@%3:%4\nAuth: %5") + .arg(profile.protocol, + profile.username.isEmpty() ? QStringLiteral("") : profile.username, + profile.host, + QString::number(profile.port), + profile.authMode)); + m_profileCache.insert_or_assign(profile.id, profile); } } -std::optional ProfilesWindow::selectedProfileId() const +std::optional ProfilesWindow::selectedProfile() const { QListWidgetItem* item = m_profilesList->currentItem(); if (item == nullptr) { @@ -117,27 +141,31 @@ std::optional ProfilesWindow::selectedProfileId() const return std::nullopt; } - return value.toLongLong(); + const qint64 id = value.toLongLong(); + const auto cacheIt = m_profileCache.find(id); + if (cacheIt != m_profileCache.end()) { + return cacheIt->second; + } + + return m_repository->getProfile(id); } void ProfilesWindow::createProfile() { - bool accepted = false; - const QString name = QInputDialog::getText(this, - QStringLiteral("New Profile"), - QStringLiteral("Profile name:"), - QLineEdit::Normal, - QString(), - &accepted); + ProfileDialog dialog(this); + dialog.setDialogTitle(QStringLiteral("New Profile")); - if (!accepted || name.trimmed().isEmpty()) { + if (dialog.exec() != QDialog::Accepted) { return; } - if (!m_repository->createProfile(name).has_value()) { + if (!m_repository->createProfile(dialog.profile()).has_value()) { QMessageBox::warning(this, QStringLiteral("Create Profile"), - QStringLiteral("Failed to create profile. Names must be unique.")); + QStringLiteral("Failed to create profile: %1") + .arg(m_repository->lastError().isEmpty() + ? QStringLiteral("unknown error") + : m_repository->lastError())); return; } @@ -146,31 +174,32 @@ void ProfilesWindow::createProfile() void ProfilesWindow::editSelectedProfile() { - const std::optional profileId = selectedProfileId(); - QListWidgetItem* currentItem = m_profilesList->currentItem(); - if (!profileId.has_value() || currentItem == nullptr) { + const std::optional selected = selectedProfile(); + if (!selected.has_value()) { QMessageBox::information(this, QStringLiteral("Edit Profile"), QStringLiteral("Select a profile first.")); return; } - bool accepted = false; - const QString name = QInputDialog::getText(this, - QStringLiteral("Edit Profile"), - QStringLiteral("Profile name:"), - QLineEdit::Normal, - currentItem->text(), - &accepted); + ProfileDialog dialog(this); + dialog.setDialogTitle(QStringLiteral("Edit Profile")); + dialog.setProfile(selected.value()); - if (!accepted || name.trimmed().isEmpty()) { + if (dialog.exec() != QDialog::Accepted) { return; } - if (!m_repository->updateProfile(profileId.value(), name)) { + Profile updated = dialog.profile(); + updated.id = selected->id; + + if (!m_repository->updateProfile(updated)) { QMessageBox::warning(this, QStringLiteral("Edit Profile"), - QStringLiteral("Failed to update profile. Names must be unique.")); + QStringLiteral("Failed to update profile: %1") + .arg(m_repository->lastError().isEmpty() + ? QStringLiteral("unknown error") + : m_repository->lastError())); return; } @@ -179,9 +208,8 @@ void ProfilesWindow::editSelectedProfile() void ProfilesWindow::deleteSelectedProfile() { - const std::optional profileId = selectedProfileId(); - QListWidgetItem* currentItem = m_profilesList->currentItem(); - if (!profileId.has_value() || currentItem == nullptr) { + const std::optional selected = selectedProfile(); + if (!selected.has_value()) { QMessageBox::information(this, QStringLiteral("Delete Profile"), QStringLiteral("Select a profile first.")); @@ -191,7 +219,7 @@ void ProfilesWindow::deleteSelectedProfile() const QMessageBox::StandardButton confirm = QMessageBox::question( this, QStringLiteral("Delete Profile"), - QStringLiteral("Delete profile '%1'?").arg(currentItem->text()), + QStringLiteral("Delete profile '%1'?").arg(selected->name), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); @@ -199,10 +227,13 @@ void ProfilesWindow::deleteSelectedProfile() return; } - if (!m_repository->deleteProfile(profileId.value())) { + if (!m_repository->deleteProfile(selected->id)) { QMessageBox::warning(this, QStringLiteral("Delete Profile"), - QStringLiteral("Failed to delete profile.")); + QStringLiteral("Failed to delete profile: %1") + .arg(m_repository->lastError().isEmpty() + ? QStringLiteral("unknown error") + : m_repository->lastError())); return; } @@ -215,7 +246,24 @@ void ProfilesWindow::openSessionForItem(QListWidgetItem* item) return; } - auto* session = new SessionWindow(item->text()); + const QVariant value = item->data(Qt::UserRole); + if (!value.isValid()) { + return; + } + + const qint64 id = value.toLongLong(); + const std::optional profile = m_repository->getProfile(id); + if (!profile.has_value()) { + QMessageBox::warning(this, + QStringLiteral("Connect"), + QStringLiteral("Failed to load profile for session: %1") + .arg(m_repository->lastError().isEmpty() + ? QStringLiteral("profile not found") + : m_repository->lastError())); + return; + } + + auto* session = new SessionWindow(profile.value()); session->setAttribute(Qt::WA_DeleteOnClose); m_sessionWindows.emplace_back(session); diff --git a/src/profiles_window.h b/src/profiles_window.h index 36c3c46..b1c6e1c 100644 --- a/src/profiles_window.h +++ b/src/profiles_window.h @@ -1,6 +1,8 @@ #ifndef ORBITHUB_PROFILES_WINDOW_H #define ORBITHUB_PROFILES_WINDOW_H +#include "profile_repository.h" + #include #include #include @@ -8,6 +10,7 @@ #include #include #include +#include #include class QListWidget; @@ -15,7 +18,6 @@ class QListWidgetItem; class QLineEdit; class QPushButton; class SessionWindow; -class ProfileRepository; class ProfilesWindow : public QMainWindow { @@ -33,10 +35,11 @@ private: QPushButton* m_deleteButton; std::vector> m_sessionWindows; std::unique_ptr m_repository; + std::unordered_map m_profileCache; void setupUi(); void loadProfiles(const QString& query = QString()); - std::optional selectedProfileId() const; + std::optional selectedProfile() const; void createProfile(); void editSelectedProfile(); void deleteSelectedProfile(); diff --git a/src/session_window.cpp b/src/session_window.cpp index c6c6fb7..cc758a2 100644 --- a/src/session_window.cpp +++ b/src/session_window.cpp @@ -3,43 +3,93 @@ #include #include #include +#include #include #include -SessionWindow::SessionWindow(const QString& profileName, QWidget* parent) +SessionWindow::SessionWindow(const Profile& profile, QWidget* parent) : QMainWindow(parent), m_tabs(new QTabWidget(this)) { - setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profileName)); - resize(900, 600); + setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profile.name)); + resize(980, 680); + + m_tabs->setTabsClosable(true); + connect(m_tabs, + &QTabWidget::tabCloseRequested, + this, + [this](int index) { + QWidget* tab = m_tabs->widget(index); + m_tabs->removeTab(index); + delete tab; + if (m_tabs->count() == 0) { + close(); + } + }); setCentralWidget(m_tabs); - addPlaceholderTab(profileName); + addSessionTab(profile); } -void SessionWindow::addPlaceholderTab(const QString& profileName) +void SessionWindow::addSessionTab(const Profile& profile) { auto* container = new QWidget(this); auto* layout = new QVBoxLayout(container); - auto* titleLabel = new QLabel(QStringLiteral("Profile: %1").arg(profileName), container); + auto* profileLabel = new QLabel(QStringLiteral("Profile: %1").arg(profile.name), container); + auto* endpointLabel = new QLabel( + QStringLiteral("Endpoint: %1://%2@%3:%4") + .arg(profile.protocol, + profile.username.isEmpty() ? QStringLiteral("") : profile.username, + profile.host, + QString::number(profile.port)), + container); + auto* authModeLabel = new QLabel(QStringLiteral("Auth Mode: %1").arg(profile.authMode), container); + auto* statusLabel = new QLabel(QStringLiteral("Connection State: Connecting"), container); auto* surfaceLabel = new QLabel(QStringLiteral("OrbitHub Native Surface"), container); - QFont titleFont = titleLabel->font(); - titleFont.setBold(true); - titleLabel->setFont(titleFont); + QFont profileFont = profileLabel->font(); + profileFont.setBold(true); + profileLabel->setFont(profileFont); QFont surfaceFont = surfaceLabel->font(); surfaceFont.setPointSize(surfaceFont.pointSize() + 6); surfaceFont.setBold(true); surfaceLabel->setFont(surfaceFont); + statusLabel->setStyleSheet( + QStringLiteral("border: 1px solid #a5a5a5; background-color: #fff3cd; padding: 6px;")); + surfaceLabel->setAlignment(Qt::AlignCenter); - surfaceLabel->setMinimumHeight(200); + surfaceLabel->setMinimumHeight(220); surfaceLabel->setStyleSheet( QStringLiteral("border: 1px solid #8a8a8a; background-color: #f5f5f5;")); - layout->addWidget(titleLabel); + layout->addWidget(profileLabel); + layout->addWidget(endpointLabel); + layout->addWidget(authModeLabel); + layout->addWidget(statusLabel); layout->addWidget(surfaceLabel, 1); - m_tabs->addTab(container, profileName); + const int tabIndex = m_tabs->addTab(container, QStringLiteral("%1 (Connecting)").arg(profile.name)); + + QTimer::singleShot(900, + this, + [this, tabIndex, statusLabel, profile]() { + const bool shouldFail = profile.host.contains(QStringLiteral("fail"), + Qt::CaseInsensitive); + if (shouldFail) { + statusLabel->setText(QStringLiteral("Connection State: Failed")); + statusLabel->setStyleSheet(QStringLiteral( + "border: 1px solid #a94442; background-color: #f2dede; padding: 6px;")); + m_tabs->setTabText(tabIndex, + QStringLiteral("%1 (Failed)").arg(profile.name)); + return; + } + + statusLabel->setText(QStringLiteral("Connection State: Connected")); + statusLabel->setStyleSheet(QStringLiteral( + "border: 1px solid #3c763d; background-color: #dff0d8; padding: 6px;")); + m_tabs->setTabText(tabIndex, + QStringLiteral("%1 (Connected)").arg(profile.name)); + }); } diff --git a/src/session_window.h b/src/session_window.h index c2a1330..1136ca6 100644 --- a/src/session_window.h +++ b/src/session_window.h @@ -1,8 +1,9 @@ #ifndef ORBITHUB_SESSION_WINDOW_H #define ORBITHUB_SESSION_WINDOW_H +#include "profile_repository.h" + #include -#include class QTabWidget; @@ -11,12 +12,12 @@ class SessionWindow : public QMainWindow Q_OBJECT public: - explicit SessionWindow(const QString& profileName, QWidget* parent = nullptr); + explicit SessionWindow(const Profile& profile, QWidget* parent = nullptr); private: QTabWidget* m_tabs; - void addPlaceholderTab(const QString& profileName); + void addSessionTab(const Profile& profile); }; #endif