Compare commits
7 Commits
v0-m0-done
...
v0-m2-done
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
582c57bc5f | ||
|
|
01762422e9 | ||
|
|
f8a81ebe36 | ||
|
|
87b0f60569 | ||
|
|
09c54313af | ||
|
|
fba7336f69 | ||
|
|
6d147894c5 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build/
|
||||||
@@ -10,18 +10,22 @@ set(CMAKE_AUTOMOC ON)
|
|||||||
set(CMAKE_AUTOUIC ON)
|
set(CMAKE_AUTOUIC ON)
|
||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
|
|
||||||
find_package(Qt6 6.2 REQUIRED COMPONENTS Widgets)
|
find_package(Qt6 6.2 REQUIRED COMPONENTS Widgets Sql)
|
||||||
|
|
||||||
qt_standard_project_setup()
|
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.h
|
||||||
src/profiles_window.cpp
|
src/profiles_window.cpp
|
||||||
src/profiles_window.h
|
src/profiles_window.h
|
||||||
src/session_window.cpp
|
src/session_window.cpp
|
||||||
src/session_window.h
|
src/session_window.h
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(orbithub PRIVATE Qt6::Widgets)
|
target_link_libraries(orbithub PRIVATE Qt6::Widgets Qt6::Sql)
|
||||||
|
|
||||||
install(TARGETS orbithub RUNTIME DESTINATION bin)
|
install(TARGETS orbithub RUNTIME DESTINATION bin)
|
||||||
|
|||||||
56
docs/BUILDING.md
Normal file
56
docs/BUILDING.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Building OrbitHub (C++ / Qt6 Widgets)
|
||||||
|
|
||||||
|
Run all commands from the repository root unless noted.
|
||||||
|
|
||||||
|
## Linux (Ubuntu / Mint)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y \
|
||||||
|
build-essential cmake ninja-build git pkg-config \
|
||||||
|
qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools
|
||||||
|
|
||||||
|
cmake -S . -B build -G Ninja
|
||||||
|
cmake --build build
|
||||||
|
./build/orbithub
|
||||||
|
```
|
||||||
|
|
||||||
|
## macOS (Homebrew)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcode-select --install
|
||||||
|
brew update
|
||||||
|
brew install cmake ninja pkg-config qt@6
|
||||||
|
|
||||||
|
cmake -S . -B build -G Ninja -DCMAKE_PREFIX_PATH="$(brew --prefix qt@6)"
|
||||||
|
cmake --build build
|
||||||
|
./build/orbithub
|
||||||
|
```
|
||||||
|
|
||||||
|
## Windows 11 (PowerShell + MSVC + vcpkg)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
winget install -e --id Git.Git
|
||||||
|
winget install -e --id Kitware.CMake
|
||||||
|
winget install -e --id Ninja-build.Ninja
|
||||||
|
winget install -e --id Microsoft.VisualStudio.2022.BuildTools `
|
||||||
|
--override "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools"
|
||||||
|
```
|
||||||
|
|
||||||
|
Open a new terminal after installs, then:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git clone https://github.com/microsoft/vcpkg C:\dev\vcpkg
|
||||||
|
C:\dev\vcpkg\bootstrap-vcpkg.bat
|
||||||
|
C:\dev\vcpkg\vcpkg.exe install qtbase:x64-windows
|
||||||
|
|
||||||
|
cmake -S . -B build -G Ninja `
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake
|
||||||
|
cmake --build build
|
||||||
|
.\build\orbithub.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- OrbitHub currently requires Qt6 Widgets and CMake 3.21+.
|
||||||
|
- If Qt is installed in a custom location, pass `-DCMAKE_PREFIX_PATH=/path/to/Qt/6.x.x/<toolchain>` to CMake.
|
||||||
@@ -54,3 +54,13 @@ OrbitHub uses a two-window model:
|
|||||||
- Profiles CRUD
|
- Profiles CRUD
|
||||||
- Connect loads profile name into tab
|
- Connect loads profile name into tab
|
||||||
- Tag: v0-m1-done
|
- Tag: v0-m1-done
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Milestone 2
|
||||||
|
|
||||||
|
- Extend profiles schema (`host`, `port`, `username`, `protocol`, `auth_mode`)
|
||||||
|
- Replace prompt-based create/edit with a structured profile dialog form
|
||||||
|
- Connect loads full profile details into session tab
|
||||||
|
- Session lifecycle states in UI (`Connecting`, `Connected`, `Failed`) with non-blocking updates
|
||||||
|
- Tag: v0-m2-done
|
||||||
|
|||||||
@@ -11,7 +11,35 @@ Delivered:
|
|||||||
- `SessionWindow` (`QMainWindow`) with `QTabWidget`
|
- `SessionWindow` (`QMainWindow`) with `QTabWidget`
|
||||||
- Placeholder tab content showing `OrbitHub Native Surface`
|
- Placeholder tab content showing `OrbitHub Native Surface`
|
||||||
- `main.cpp` wiring for application startup
|
- `main.cpp` wiring for application startup
|
||||||
|
- Cross-platform build command guide in `docs/BUILDING.md`
|
||||||
|
|
||||||
Git:
|
Git:
|
||||||
- Branch: `milestone-0-restart-cpp`
|
|
||||||
- Tag: `v0-m0-done`
|
- Tag: `v0-m0-done`
|
||||||
|
|
||||||
|
## Milestone 1 - Storage and CRUD
|
||||||
|
|
||||||
|
Status: Completed
|
||||||
|
|
||||||
|
Delivered:
|
||||||
|
- SQLite integration via Qt SQL (`QSQLITE`)
|
||||||
|
- Persistent profile database bootstrap (`profiles` table)
|
||||||
|
- Profiles CRUD (New / Edit / Delete) in `ProfilesWindow`
|
||||||
|
- Search-backed profile listing from storage
|
||||||
|
- Double-click connect opens `SessionWindow` tab with selected profile name
|
||||||
|
|
||||||
|
Git:
|
||||||
|
- Tag: `v0-m1-done`
|
||||||
|
|
||||||
|
## Milestone 2 - Profile Details and Connect Lifecycle
|
||||||
|
|
||||||
|
Status: Completed
|
||||||
|
|
||||||
|
Delivered:
|
||||||
|
- SQLite schema migration for profile details (`host`, `port`, `username`, `protocol`, `auth_mode`)
|
||||||
|
- New `ProfileDialog` form for New/Edit profile workflows
|
||||||
|
- Profiles list now shows endpoint metadata and supports search by name or host
|
||||||
|
- Connect now loads complete profile details into `SessionWindow`
|
||||||
|
- Session tab lifecycle status updates (`Connecting`, `Connected`, `Failed`) via non-blocking timer flow
|
||||||
|
|
||||||
|
Git:
|
||||||
|
- Tag: `v0-m2-done`
|
||||||
|
|||||||
124
src/profile_dialog.cpp
Normal file
124
src/profile_dialog.cpp
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#include "profile_dialog.h"
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QSignalBlocker>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
int standardPortForProtocol(const QString& protocol)
|
||||||
|
{
|
||||||
|
if (protocol == QStringLiteral("RDP")) {
|
||||||
|
return 3389;
|
||||||
|
}
|
||||||
|
if (protocol == QStringLiteral("VNC")) {
|
||||||
|
return 5900;
|
||||||
|
}
|
||||||
|
return 22; // SSH default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")});
|
||||||
|
|
||||||
|
connect(m_protocolInput,
|
||||||
|
&QComboBox::currentTextChanged,
|
||||||
|
this,
|
||||||
|
[this](const QString& protocol) {
|
||||||
|
m_portInput->setValue(standardPortForProtocol(protocol));
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
{
|
||||||
|
// Keep stored custom port when loading an existing profile.
|
||||||
|
const QSignalBlocker blocker(m_protocolInput);
|
||||||
|
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
35
src/profile_dialog.h
Normal 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
|
||||||
310
src/profile_repository.cpp
Normal file
310
src/profile_repository.cpp
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
#include "profile_repository.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QSqlDatabase>
|
||||||
|
#include <QSqlError>
|
||||||
|
#include <QSqlQuery>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
QString buildDatabasePath()
|
||||||
|
{
|
||||||
|
QString appDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||||
|
if (appDataPath.isEmpty()) {
|
||||||
|
appDataPath = QDir::currentPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir dataDir(appDataPath);
|
||||||
|
dataDir.mkpath(QStringLiteral("."));
|
||||||
|
|
||||||
|
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"))
|
||||||
|
{
|
||||||
|
if (!initializeDatabase()) {
|
||||||
|
QSqlDatabase::removeDatabase(m_connectionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileRepository::~ProfileRepository()
|
||||||
|
{
|
||||||
|
if (QSqlDatabase::contains(m_connectionName)) {
|
||||||
|
QSqlDatabase db = QSqlDatabase::database(m_connectionName);
|
||||||
|
if (db.isOpen()) {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QSqlDatabase::removeDatabase(m_connectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (!QSqlDatabase::contains(m_connectionName)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastError(QString());
|
||||||
|
|
||||||
|
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||||
|
if (searchQuery.trimmed().isEmpty()) {
|
||||||
|
query.prepare(QStringLiteral(
|
||||||
|
"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, 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"));
|
||||||
|
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(profileFromQuery(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Profile> ProfileRepository::getProfile(qint64 id) const
|
||||||
|
{
|
||||||
|
if (!QSqlDatabase::contains(m_connectionName)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastError(QString());
|
||||||
|
|
||||||
|
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.numRowsAffected() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProfileRepository::deleteProfile(qint64 id) const
|
||||||
|
{
|
||||||
|
if (!QSqlDatabase::contains(m_connectionName)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.numRowsAffected() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProfileRepository::initializeDatabase()
|
||||||
|
{
|
||||||
|
QSqlDatabase database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName);
|
||||||
|
database.setDatabaseName(buildDatabasePath());
|
||||||
|
|
||||||
|
if (!database.open()) {
|
||||||
|
m_initError = database.lastError().text();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlQuery query(database);
|
||||||
|
const bool created = query.exec(QStringLiteral(
|
||||||
|
"CREATE TABLE IF NOT EXISTS profiles ("
|
||||||
|
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||||
|
"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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
46
src/profile_repository.h
Normal file
46
src/profile_repository.h
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#ifndef ORBITHUB_PROFILE_REPOSITORY_H
|
||||||
|
#define ORBITHUB_PROFILE_REPOSITORY_H
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct Profile
|
||||||
|
{
|
||||||
|
qint64 id = -1;
|
||||||
|
QString name;
|
||||||
|
QString host;
|
||||||
|
int port = 22;
|
||||||
|
QString username;
|
||||||
|
QString protocol = QStringLiteral("SSH");
|
||||||
|
QString authMode = QStringLiteral("Password");
|
||||||
|
};
|
||||||
|
|
||||||
|
class ProfileRepository
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ProfileRepository();
|
||||||
|
~ProfileRepository();
|
||||||
|
|
||||||
|
QString initError() const;
|
||||||
|
QString lastError() const;
|
||||||
|
|
||||||
|
std::vector<Profile> listProfiles(const QString& searchQuery = QString()) 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
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "profiles_window.h"
|
#include "profiles_window.h"
|
||||||
|
|
||||||
|
#include "profile_dialog.h"
|
||||||
|
#include "profile_repository.h"
|
||||||
#include "session_window.h"
|
#include "session_window.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -12,25 +14,50 @@
|
|||||||
#include <QListWidgetItem>
|
#include <QListWidgetItem>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QStringList>
|
#include <QVariant>
|
||||||
#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),
|
||||||
m_profilesList(nullptr),
|
m_profilesList(nullptr),
|
||||||
m_newButton(nullptr),
|
m_newButton(nullptr),
|
||||||
m_editButton(nullptr),
|
m_editButton(nullptr),
|
||||||
m_deleteButton(nullptr)
|
m_deleteButton(nullptr),
|
||||||
|
m_repository(std::make_unique<ProfileRepository>())
|
||||||
{
|
{
|
||||||
setWindowTitle(QStringLiteral("OrbitHub Profiles"));
|
setWindowTitle(QStringLiteral("OrbitHub Profiles"));
|
||||||
resize(520, 620);
|
resize(640, 620);
|
||||||
|
|
||||||
setupUi();
|
setupUi();
|
||||||
populateSampleProfiles();
|
|
||||||
|
if (!m_repository->initError().isEmpty()) {
|
||||||
|
QMessageBox::critical(this,
|
||||||
|
QStringLiteral("Database Error"),
|
||||||
|
QStringLiteral("Failed to initialize SQLite database: %1")
|
||||||
|
.arg(m_repository->initError()));
|
||||||
|
m_newButton->setEnabled(false);
|
||||||
|
m_editButton->setEnabled(false);
|
||||||
|
m_deleteButton->setEnabled(false);
|
||||||
|
m_searchBox->setEnabled(false);
|
||||||
|
m_profilesList->setEnabled(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfilesWindow::~ProfilesWindow() = default;
|
||||||
|
|
||||||
void ProfilesWindow::setupUi()
|
void ProfilesWindow::setupUi()
|
||||||
{
|
{
|
||||||
auto* central = new QWidget(this);
|
auto* central = new QWidget(this);
|
||||||
@@ -38,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);
|
||||||
@@ -63,49 +90,154 @@ void ProfilesWindow::setupUi()
|
|||||||
connect(m_searchBox,
|
connect(m_searchBox,
|
||||||
&QLineEdit::textChanged,
|
&QLineEdit::textChanged,
|
||||||
this,
|
this,
|
||||||
[this](const QString& text) { filterProfiles(text); });
|
[this](const QString& text) { loadProfiles(text); });
|
||||||
|
|
||||||
connect(m_profilesList,
|
connect(m_profilesList,
|
||||||
&QListWidget::itemDoubleClicked,
|
&QListWidget::itemDoubleClicked,
|
||||||
this,
|
this,
|
||||||
[this](QListWidgetItem* item) { openSessionForItem(item); });
|
[this](QListWidgetItem* item) { openSessionForItem(item); });
|
||||||
|
|
||||||
// Milestone 0 keeps profile management as placeholders.
|
connect(m_newButton, &QPushButton::clicked, this, [this]() { createProfile(); });
|
||||||
auto showTodo = [this](const QString& action) {
|
connect(m_editButton, &QPushButton::clicked, this, [this]() { editSelectedProfile(); });
|
||||||
|
connect(m_deleteButton, &QPushButton::clicked, this, [this]() { deleteSelectedProfile(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
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(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<Profile> ProfilesWindow::selectedProfile() const
|
||||||
|
{
|
||||||
|
QListWidgetItem* item = m_profilesList->currentItem();
|
||||||
|
if (item == nullptr) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariant value = item->data(Qt::UserRole);
|
||||||
|
if (!value.isValid()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
ProfileDialog dialog(this);
|
||||||
|
dialog.setDialogTitle(QStringLiteral("New Profile"));
|
||||||
|
|
||||||
|
if (dialog.exec() != QDialog::Accepted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_repository->createProfile(dialog.profile()).has_value()) {
|
||||||
|
QMessageBox::warning(this,
|
||||||
|
QStringLiteral("Create Profile"),
|
||||||
|
QStringLiteral("Failed to create profile: %1")
|
||||||
|
.arg(m_repository->lastError().isEmpty()
|
||||||
|
? QStringLiteral("unknown error")
|
||||||
|
: m_repository->lastError()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProfiles(m_searchBox->text());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProfilesWindow::editSelectedProfile()
|
||||||
|
{
|
||||||
|
const std::optional<Profile> selected = selectedProfile();
|
||||||
|
if (!selected.has_value()) {
|
||||||
QMessageBox::information(this,
|
QMessageBox::information(this,
|
||||||
QStringLiteral("Milestone 0"),
|
QStringLiteral("Edit Profile"),
|
||||||
QStringLiteral("%1 is planned for Milestone 1.").arg(action));
|
QStringLiteral("Select a profile first."));
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
connect(m_newButton, &QPushButton::clicked, this, [showTodo]() { showTodo(QStringLiteral("New Profile")); });
|
ProfileDialog dialog(this);
|
||||||
connect(m_editButton, &QPushButton::clicked, this, [showTodo]() { showTodo(QStringLiteral("Edit Profile")); });
|
dialog.setDialogTitle(QStringLiteral("Edit Profile"));
|
||||||
connect(m_deleteButton,
|
dialog.setProfile(selected.value());
|
||||||
&QPushButton::clicked,
|
|
||||||
|
if (dialog.exec() != QDialog::Accepted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile updated = dialog.profile();
|
||||||
|
updated.id = selected->id;
|
||||||
|
|
||||||
|
if (!m_repository->updateProfile(updated)) {
|
||||||
|
QMessageBox::warning(this,
|
||||||
|
QStringLiteral("Edit Profile"),
|
||||||
|
QStringLiteral("Failed to update profile: %1")
|
||||||
|
.arg(m_repository->lastError().isEmpty()
|
||||||
|
? QStringLiteral("unknown error")
|
||||||
|
: m_repository->lastError()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProfiles(m_searchBox->text());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProfilesWindow::deleteSelectedProfile()
|
||||||
|
{
|
||||||
|
const std::optional<Profile> selected = selectedProfile();
|
||||||
|
if (!selected.has_value()) {
|
||||||
|
QMessageBox::information(this,
|
||||||
|
QStringLiteral("Delete Profile"),
|
||||||
|
QStringLiteral("Select a profile first."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QMessageBox::StandardButton confirm = QMessageBox::question(
|
||||||
this,
|
this,
|
||||||
[showTodo]() { showTodo(QStringLiteral("Delete Profile")); });
|
QStringLiteral("Delete Profile"),
|
||||||
|
QStringLiteral("Delete profile '%1'?").arg(selected->name),
|
||||||
|
QMessageBox::Yes | QMessageBox::No,
|
||||||
|
QMessageBox::No);
|
||||||
|
|
||||||
|
if (confirm != QMessageBox::Yes) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProfilesWindow::populateSampleProfiles()
|
if (!m_repository->deleteProfile(selected->id)) {
|
||||||
{
|
QMessageBox::warning(this,
|
||||||
const QStringList sampleProfiles = {
|
QStringLiteral("Delete Profile"),
|
||||||
QStringLiteral("Production Bastion"),
|
QStringLiteral("Failed to delete profile: %1")
|
||||||
QStringLiteral("Staging API Host"),
|
.arg(m_repository->lastError().isEmpty()
|
||||||
QStringLiteral("Local Lab VM"),
|
? QStringLiteral("unknown error")
|
||||||
QStringLiteral("CI Build Agent")};
|
: m_repository->lastError()));
|
||||||
|
return;
|
||||||
m_profilesList->addItems(sampleProfiles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProfilesWindow::filterProfiles(const QString& query)
|
loadProfiles(m_searchBox->text());
|
||||||
{
|
|
||||||
const QString trimmed = query.trimmed();
|
|
||||||
|
|
||||||
for (int i = 0; i < m_profilesList->count(); ++i) {
|
|
||||||
QListWidgetItem* item = m_profilesList->item(i);
|
|
||||||
const bool matches = trimmed.isEmpty()
|
|
||||||
|| item->text().contains(trimmed, Qt::CaseInsensitive);
|
|
||||||
item->setHidden(!matches);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProfilesWindow::openSessionForItem(QListWidgetItem* item)
|
void ProfilesWindow::openSessionForItem(QListWidgetItem* item)
|
||||||
@@ -114,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);
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
#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 <memory>
|
||||||
|
#include <optional>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
class QListWidget;
|
class QListWidget;
|
||||||
@@ -19,6 +25,7 @@ class ProfilesWindow : public QMainWindow
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ProfilesWindow(QWidget* parent = nullptr);
|
explicit ProfilesWindow(QWidget* parent = nullptr);
|
||||||
|
~ProfilesWindow() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QLineEdit* m_searchBox;
|
QLineEdit* m_searchBox;
|
||||||
@@ -27,10 +34,15 @@ private:
|
|||||||
QPushButton* m_editButton;
|
QPushButton* m_editButton;
|
||||||
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::unordered_map<qint64, Profile> m_profileCache;
|
||||||
|
|
||||||
void setupUi();
|
void setupUi();
|
||||||
void populateSampleProfiles();
|
void loadProfiles(const QString& query = QString());
|
||||||
void filterProfiles(const QString& query);
|
std::optional<Profile> selectedProfile() const;
|
||||||
|
void createProfile();
|
||||||
|
void editSelectedProfile();
|
||||||
|
void deleteSelectedProfile();
|
||||||
void openSessionForItem(QListWidgetItem* item);
|
void openSessionForItem(QListWidgetItem* item);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user