Implement Milestone 2 profile schema, dialog, and connect lifecycle

This commit is contained in:
Keith Smith
2026-03-01 09:21:53 -07:00
parent 87b0f60569
commit f8a81ebe36
9 changed files with 488 additions and 92 deletions

View File

@@ -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

99
src/profile_dialog.cpp Normal file
View File

@@ -0,0 +1,99 @@
#include "profile_dialog.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QSpinBox>
#include <QVBoxLayout>
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();
}

35
src/profile_dialog.h Normal file
View File

@@ -0,0 +1,35 @@
#ifndef ORBITHUB_PROFILE_DIALOG_H
#define ORBITHUB_PROFILE_DIALOG_H
#include "profile_repository.h"
#include <QDialog>
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

View File

@@ -1,6 +1,7 @@
#include "profile_repository.h"
#include <QDir>
#include <QSet>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
@@ -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<Profile> ProfileRepository::listProfiles(const QString& searchQuery) const
{
std::vector<Profile> result;
@@ -53,68 +88,115 @@ std::vector<Profile> 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<Profile> ProfileRepository::createProfile(const QString& name) const
std::optional<Profile> 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<Profile> 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<QString> columns;
while (tableInfo.next()) {
columns.insert(tableInfo.value(1).toString());
}
struct ColumnDef {
QString name;
QString ddl;
};
const std::vector<ColumnDef> 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;
}

View File

@@ -2,14 +2,20 @@
#define ORBITHUB_PROFILE_REPOSITORY_H
#include <QString>
#include <QtGlobal>
#include <optional>
#include <vector>
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<Profile> listProfiles(const QString& searchQuery = QString()) const;
std::optional<Profile> createProfile(const QString& name) const;
bool updateProfile(qint64 id, const QString& name) const;
std::optional<Profile> getProfile(qint64 id) const;
std::optional<Profile> 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

View File

@@ -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 <QAbstractItemView>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
@@ -18,6 +18,14 @@
#include <QVBoxLayout>
#include <QWidget>
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<ProfileRepository>())
{
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<Profile> 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("<none>") : profile.username,
profile.host,
QString::number(profile.port),
profile.authMode));
m_profileCache.insert_or_assign(profile.id, profile);
}
}
std::optional<qint64> ProfilesWindow::selectedProfileId() const
std::optional<Profile> ProfilesWindow::selectedProfile() const
{
QListWidgetItem* item = m_profilesList->currentItem();
if (item == nullptr) {
@@ -117,27 +141,31 @@ std::optional<qint64> 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<qint64> profileId = selectedProfileId();
QListWidgetItem* currentItem = m_profilesList->currentItem();
if (!profileId.has_value() || currentItem == nullptr) {
const std::optional<Profile> 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<qint64> profileId = selectedProfileId();
QListWidgetItem* currentItem = m_profilesList->currentItem();
if (!profileId.has_value() || currentItem == nullptr) {
const std::optional<Profile> 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> 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);

View File

@@ -1,6 +1,8 @@
#ifndef ORBITHUB_PROFILES_WINDOW_H
#define ORBITHUB_PROFILES_WINDOW_H
#include "profile_repository.h"
#include <QMainWindow>
#include <QString>
#include <QtGlobal>
@@ -8,6 +10,7 @@
#include <memory>
#include <optional>
#include <QPointer>
#include <unordered_map>
#include <vector>
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<QPointer<SessionWindow>> m_sessionWindows;
std::unique_ptr<ProfileRepository> m_repository;
std::unordered_map<qint64, Profile> m_profileCache;
void setupUi();
void loadProfiles(const QString& query = QString());
std::optional<qint64> selectedProfileId() const;
std::optional<Profile> selectedProfile() const;
void createProfile();
void editSelectedProfile();
void deleteSelectedProfile();

View File

@@ -3,43 +3,93 @@
#include <QFont>
#include <QLabel>
#include <QTabWidget>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>
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("<none>") : 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));
});
}

View File

@@ -1,8 +1,9 @@
#ifndef ORBITHUB_SESSION_WINDOW_H
#define ORBITHUB_SESSION_WINDOW_H
#include "profile_repository.h"
#include <QMainWindow>
#include <QString>
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