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 add_executable(orbithub
src/main.cpp src/main.cpp
src/profile_dialog.cpp
src/profile_dialog.h
src/profile_repository.cpp src/profile_repository.cpp
src/profile_repository.h src/profile_repository.h
src/profiles_window.cpp 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 "profile_repository.h"
#include <QDir> #include <QDir>
#include <QSet>
#include <QSqlDatabase> #include <QSqlDatabase>
#include <QSqlError> #include <QSqlError>
#include <QSqlQuery> #include <QSqlQuery>
@@ -20,6 +21,35 @@ QString buildDatabasePath()
return dataDir.filePath(QStringLiteral("orbithub_profiles.sqlite")); 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")) ProfileRepository::ProfileRepository() : m_connectionName(QStringLiteral("orbithub_main"))
@@ -45,6 +75,11 @@ QString ProfileRepository::initError() const
return m_initError; return m_initError;
} }
QString ProfileRepository::lastError() const
{
return m_lastError;
}
std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery) const std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery) const
{ {
std::vector<Profile> result; std::vector<Profile> result;
@@ -53,68 +88,115 @@ std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery)
return result; return result;
} }
setLastError(QString());
QSqlQuery query(QSqlDatabase::database(m_connectionName)); QSqlQuery query(QSqlDatabase::database(m_connectionName));
if (searchQuery.trimmed().isEmpty()) { if (searchQuery.trimmed().isEmpty()) {
query.prepare(QStringLiteral( 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 { } else {
query.prepare(QStringLiteral( query.prepare(QStringLiteral(
"SELECT id, name FROM profiles " "SELECT id, name, host, port, username, protocol, auth_mode "
"WHERE lower(name) LIKE lower(?) " "FROM profiles "
"WHERE lower(name) LIKE lower(?) OR lower(host) LIKE lower(?) "
"ORDER BY lower(name) ASC, id ASC")); "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()) { if (!query.exec()) {
setLastError(query.lastError().text());
return result; return result;
} }
while (query.next()) { while (query.next()) {
result.push_back(Profile{query.value(0).toLongLong(), query.value(1).toString()}); result.push_back(profileFromQuery(query));
} }
return result; return result;
} }
std::optional<Profile> ProfileRepository::createProfile(const QString& name) const std::optional<Profile> ProfileRepository::getProfile(qint64 id) const
{ {
if (!QSqlDatabase::contains(m_connectionName)) { if (!QSqlDatabase::contains(m_connectionName)) {
return std::nullopt; return std::nullopt;
} }
const QString trimmedName = name.trimmed(); setLastError(QString());
if (trimmedName.isEmpty()) {
return std::nullopt;
}
QSqlQuery query(QSqlDatabase::database(m_connectionName)); QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral("INSERT INTO profiles(name) VALUES (?)")); query.prepare(QStringLiteral(
query.addBindValue(trimmedName); "SELECT id, name, host, port, username, protocol, auth_mode "
"FROM profiles WHERE id = ?"));
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.addBindValue(id); query.addBindValue(id);
if (!query.exec()) { 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; return false;
} }
@@ -127,11 +209,14 @@ bool ProfileRepository::deleteProfile(qint64 id) const
return false; return false;
} }
setLastError(QString());
QSqlQuery query(QSqlDatabase::database(m_connectionName)); QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral("DELETE FROM profiles WHERE id = ?")); query.prepare(QStringLiteral("DELETE FROM profiles WHERE id = ?"));
query.addBindValue(id); query.addBindValue(id);
if (!query.exec()) { if (!query.exec()) {
setLastError(query.lastError().text());
return false; return false;
} }
@@ -152,12 +237,74 @@ bool ProfileRepository::initializeDatabase()
const bool created = query.exec(QStringLiteral( const bool created = query.exec(QStringLiteral(
"CREATE TABLE IF NOT EXISTS profiles (" "CREATE TABLE IF NOT EXISTS profiles ("
"id INTEGER PRIMARY KEY AUTOINCREMENT," "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) { if (!created) {
m_initError = query.lastError().text(); 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 #define ORBITHUB_PROFILE_REPOSITORY_H
#include <QString> #include <QString>
#include <QtGlobal>
#include <optional> #include <optional>
#include <vector> #include <vector>
struct Profile struct Profile
{ {
qint64 id; qint64 id = -1;
QString name; QString name;
QString host;
int port = 22;
QString username;
QString protocol = QStringLiteral("SSH");
QString authMode = QStringLiteral("Password");
}; };
class ProfileRepository class ProfileRepository
@@ -19,17 +25,22 @@ public:
~ProfileRepository(); ~ProfileRepository();
QString initError() const; QString initError() const;
QString lastError() const;
std::vector<Profile> listProfiles(const QString& searchQuery = QString()) const; std::vector<Profile> listProfiles(const QString& searchQuery = QString()) const;
std::optional<Profile> createProfile(const QString& name) const; std::optional<Profile> getProfile(qint64 id) const;
bool updateProfile(qint64 id, const QString& name) const; std::optional<Profile> createProfile(const Profile& profile) const;
bool updateProfile(const Profile& profile) const;
bool deleteProfile(qint64 id) const; bool deleteProfile(qint64 id) const;
private: private:
QString m_connectionName; QString m_connectionName;
QString m_initError; QString m_initError;
mutable QString m_lastError;
bool initializeDatabase(); bool initializeDatabase();
bool ensureProfileSchema() const;
void setLastError(const QString& error) const;
}; };
#endif #endif

View File

@@ -1,5 +1,6 @@
#include "profiles_window.h" #include "profiles_window.h"
#include "profile_dialog.h"
#include "profile_repository.h" #include "profile_repository.h"
#include "session_window.h" #include "session_window.h"
@@ -7,7 +8,6 @@
#include <QAbstractItemView> #include <QAbstractItemView>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QListWidget> #include <QListWidget>
@@ -18,6 +18,14 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #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) ProfilesWindow::ProfilesWindow(QWidget* parent)
: QMainWindow(parent), : QMainWindow(parent),
m_searchBox(nullptr), m_searchBox(nullptr),
@@ -28,7 +36,7 @@ ProfilesWindow::ProfilesWindow(QWidget* parent)
m_repository(std::make_unique<ProfileRepository>()) m_repository(std::make_unique<ProfileRepository>())
{ {
setWindowTitle(QStringLiteral("OrbitHub Profiles")); setWindowTitle(QStringLiteral("OrbitHub Profiles"));
resize(520, 620); resize(640, 620);
setupUi(); setupUi();
@@ -57,7 +65,7 @@ void ProfilesWindow::setupUi()
auto* searchLabel = new QLabel(QStringLiteral("Search"), central); auto* searchLabel = new QLabel(QStringLiteral("Search"), central);
m_searchBox = new QLineEdit(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 = new QListWidget(central);
m_profilesList->setSelectionMode(QAbstractItemView::SingleSelection); m_profilesList->setSelectionMode(QAbstractItemView::SingleSelection);
@@ -97,15 +105,31 @@ void ProfilesWindow::setupUi()
void ProfilesWindow::loadProfiles(const QString& query) void ProfilesWindow::loadProfiles(const QString& query)
{ {
m_profilesList->clear(); m_profilesList->clear();
m_profileCache.clear();
const std::vector<Profile> profiles = m_repository->listProfiles(query); 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) { 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->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(); QListWidgetItem* item = m_profilesList->currentItem();
if (item == nullptr) { if (item == nullptr) {
@@ -117,27 +141,31 @@ std::optional<qint64> ProfilesWindow::selectedProfileId() const
return std::nullopt; 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() void ProfilesWindow::createProfile()
{ {
bool accepted = false; ProfileDialog dialog(this);
const QString name = QInputDialog::getText(this, dialog.setDialogTitle(QStringLiteral("New Profile"));
QStringLiteral("New Profile"),
QStringLiteral("Profile name:"),
QLineEdit::Normal,
QString(),
&accepted);
if (!accepted || name.trimmed().isEmpty()) { if (dialog.exec() != QDialog::Accepted) {
return; return;
} }
if (!m_repository->createProfile(name).has_value()) { if (!m_repository->createProfile(dialog.profile()).has_value()) {
QMessageBox::warning(this, QMessageBox::warning(this,
QStringLiteral("Create Profile"), 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; return;
} }
@@ -146,31 +174,32 @@ void ProfilesWindow::createProfile()
void ProfilesWindow::editSelectedProfile() void ProfilesWindow::editSelectedProfile()
{ {
const std::optional<qint64> profileId = selectedProfileId(); const std::optional<Profile> selected = selectedProfile();
QListWidgetItem* currentItem = m_profilesList->currentItem(); if (!selected.has_value()) {
if (!profileId.has_value() || currentItem == nullptr) {
QMessageBox::information(this, QMessageBox::information(this,
QStringLiteral("Edit Profile"), QStringLiteral("Edit Profile"),
QStringLiteral("Select a profile first.")); QStringLiteral("Select a profile first."));
return; return;
} }
bool accepted = false; ProfileDialog dialog(this);
const QString name = QInputDialog::getText(this, dialog.setDialogTitle(QStringLiteral("Edit Profile"));
QStringLiteral("Edit Profile"), dialog.setProfile(selected.value());
QStringLiteral("Profile name:"),
QLineEdit::Normal,
currentItem->text(),
&accepted);
if (!accepted || name.trimmed().isEmpty()) { if (dialog.exec() != QDialog::Accepted) {
return; return;
} }
if (!m_repository->updateProfile(profileId.value(), name)) { Profile updated = dialog.profile();
updated.id = selected->id;
if (!m_repository->updateProfile(updated)) {
QMessageBox::warning(this, QMessageBox::warning(this,
QStringLiteral("Edit Profile"), 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; return;
} }
@@ -179,9 +208,8 @@ void ProfilesWindow::editSelectedProfile()
void ProfilesWindow::deleteSelectedProfile() void ProfilesWindow::deleteSelectedProfile()
{ {
const std::optional<qint64> profileId = selectedProfileId(); const std::optional<Profile> selected = selectedProfile();
QListWidgetItem* currentItem = m_profilesList->currentItem(); if (!selected.has_value()) {
if (!profileId.has_value() || currentItem == nullptr) {
QMessageBox::information(this, QMessageBox::information(this,
QStringLiteral("Delete Profile"), QStringLiteral("Delete Profile"),
QStringLiteral("Select a profile first.")); QStringLiteral("Select a profile first."));
@@ -191,7 +219,7 @@ void ProfilesWindow::deleteSelectedProfile()
const QMessageBox::StandardButton confirm = QMessageBox::question( const QMessageBox::StandardButton confirm = QMessageBox::question(
this, this,
QStringLiteral("Delete Profile"), QStringLiteral("Delete Profile"),
QStringLiteral("Delete profile '%1'?").arg(currentItem->text()), QStringLiteral("Delete profile '%1'?").arg(selected->name),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes | QMessageBox::No,
QMessageBox::No); QMessageBox::No);
@@ -199,10 +227,13 @@ void ProfilesWindow::deleteSelectedProfile()
return; return;
} }
if (!m_repository->deleteProfile(profileId.value())) { if (!m_repository->deleteProfile(selected->id)) {
QMessageBox::warning(this, QMessageBox::warning(this,
QStringLiteral("Delete Profile"), 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; return;
} }
@@ -215,7 +246,24 @@ void ProfilesWindow::openSessionForItem(QListWidgetItem* item)
return; 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); session->setAttribute(Qt::WA_DeleteOnClose);
m_sessionWindows.emplace_back(session); m_sessionWindows.emplace_back(session);

View File

@@ -1,6 +1,8 @@
#ifndef ORBITHUB_PROFILES_WINDOW_H #ifndef ORBITHUB_PROFILES_WINDOW_H
#define ORBITHUB_PROFILES_WINDOW_H #define ORBITHUB_PROFILES_WINDOW_H
#include "profile_repository.h"
#include <QMainWindow> #include <QMainWindow>
#include <QString> #include <QString>
#include <QtGlobal> #include <QtGlobal>
@@ -8,6 +10,7 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <QPointer> #include <QPointer>
#include <unordered_map>
#include <vector> #include <vector>
class QListWidget; class QListWidget;
@@ -15,7 +18,6 @@ class QListWidgetItem;
class QLineEdit; class QLineEdit;
class QPushButton; class QPushButton;
class SessionWindow; class SessionWindow;
class ProfileRepository;
class ProfilesWindow : public QMainWindow class ProfilesWindow : public QMainWindow
{ {
@@ -33,10 +35,11 @@ private:
QPushButton* m_deleteButton; QPushButton* m_deleteButton;
std::vector<QPointer<SessionWindow>> m_sessionWindows; std::vector<QPointer<SessionWindow>> m_sessionWindows;
std::unique_ptr<ProfileRepository> m_repository; std::unique_ptr<ProfileRepository> m_repository;
std::unordered_map<qint64, Profile> m_profileCache;
void setupUi(); void setupUi();
void loadProfiles(const QString& query = QString()); void loadProfiles(const QString& query = QString());
std::optional<qint64> selectedProfileId() const; std::optional<Profile> selectedProfile() const;
void createProfile(); void createProfile();
void editSelectedProfile(); void editSelectedProfile();
void deleteSelectedProfile(); void deleteSelectedProfile();

View File

@@ -3,43 +3,93 @@
#include <QFont> #include <QFont>
#include <QLabel> #include <QLabel>
#include <QTabWidget> #include <QTabWidget>
#include <QTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
SessionWindow::SessionWindow(const QString& profileName, QWidget* parent) SessionWindow::SessionWindow(const Profile& profile, QWidget* parent)
: QMainWindow(parent), m_tabs(new QTabWidget(this)) : QMainWindow(parent), m_tabs(new QTabWidget(this))
{ {
setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profileName)); setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profile.name));
resize(900, 600); 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); 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* container = new QWidget(this);
auto* layout = new QVBoxLayout(container); 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); auto* surfaceLabel = new QLabel(QStringLiteral("OrbitHub Native Surface"), container);
QFont titleFont = titleLabel->font(); QFont profileFont = profileLabel->font();
titleFont.setBold(true); profileFont.setBold(true);
titleLabel->setFont(titleFont); profileLabel->setFont(profileFont);
QFont surfaceFont = surfaceLabel->font(); QFont surfaceFont = surfaceLabel->font();
surfaceFont.setPointSize(surfaceFont.pointSize() + 6); surfaceFont.setPointSize(surfaceFont.pointSize() + 6);
surfaceFont.setBold(true); surfaceFont.setBold(true);
surfaceLabel->setFont(surfaceFont); surfaceLabel->setFont(surfaceFont);
statusLabel->setStyleSheet(
QStringLiteral("border: 1px solid #a5a5a5; background-color: #fff3cd; padding: 6px;"));
surfaceLabel->setAlignment(Qt::AlignCenter); surfaceLabel->setAlignment(Qt::AlignCenter);
surfaceLabel->setMinimumHeight(200); surfaceLabel->setMinimumHeight(220);
surfaceLabel->setStyleSheet( surfaceLabel->setStyleSheet(
QStringLiteral("border: 1px solid #8a8a8a; background-color: #f5f5f5;")); 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); 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 #ifndef ORBITHUB_SESSION_WINDOW_H
#define ORBITHUB_SESSION_WINDOW_H #define ORBITHUB_SESSION_WINDOW_H
#include "profile_repository.h"
#include <QMainWindow> #include <QMainWindow>
#include <QString>
class QTabWidget; class QTabWidget;
@@ -11,12 +12,12 @@ class SessionWindow : public QMainWindow
Q_OBJECT Q_OBJECT
public: public:
explicit SessionWindow(const QString& profileName, QWidget* parent = nullptr); explicit SessionWindow(const Profile& profile, QWidget* parent = nullptr);
private: private:
QTabWidget* m_tabs; QTabWidget* m_tabs;
void addPlaceholderTab(const QString& profileName); void addSessionTab(const Profile& profile);
}; };
#endif #endif