#include "dialogs/SchedulerDialog.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "rpc/Protocol.hpp" #include "rpc/RpcClient.hpp" #include "util/Theme.hpp" namespace velox::gui { namespace { // 0 = Sunday, matching Schedule.schema.json's daysOfWeek. constexpr const char *kDayLabels[7] = { QT_TR_NOOP("Sun"), QT_TR_NOOP("Mon"), QT_TR_NOOP("Tue"), QT_TR_NOOP("Wed"), QT_TR_NOOP("Thu"), QT_TR_NOOP("Fri"), QT_TR_NOOP("Sat"), }; } // namespace QJsonValue SchedulerDialog::buildSchedule(bool useSchedule, const QString &mode, const QString &startTime, bool runUntilDrains, const QString &stopTime, const QList &daysOfWeek, const QString &onceDate) { if (!useSchedule) { return QJsonValue(QJsonValue::Null); } QJsonObject s{ {"enabled", true}, {"mode", mode}, {"startTime", startTime}, {"stopTime", runUntilDrains ? QJsonValue(QJsonValue::Null) : QJsonValue(stopTime)}, }; if (mode == QLatin1String("periodic")) { QJsonArray days; for (int d : daysOfWeek) { days.append(d); } s["daysOfWeek"] = days; } else { s["onceDate"] = onceDate; } return s; } SchedulerDialog::SchedulerDialog(rpc::RpcClient *client, QWidget *parent) : QDialog(parent), client_(client), queueList_(new QListWidget(this)), useSchedule_(new QCheckBox(tr("Run this queue on a schedule"), this)), mode_(new QComboBox(this)), startTime_(new QTimeEdit(this)), runUntilDrains_(new QCheckBox(tr("Run until the queue drains"), this)), stopTime_(new QTimeEdit(this)), onceDate_(new QDateEdit(this)), maxConcurrent_(new QSpinBox(this)), onComplete_(new QComboBox(this)), pauseRunningOnStop_(new QCheckBox(tr("Pause running tasks too"), this)), statusLabel_(new QLabel(this)) { setWindowTitle(tr("Scheduler")); resize(640, 420); queueList_->setMaximumWidth(200); mode_->addItem(tr("Once"), QStringLiteral("once")); mode_->addItem(tr("Periodic (weekly)"), QStringLiteral("periodic")); startTime_->setDisplayFormat(QStringLiteral("HH:mm")); stopTime_->setDisplayFormat(QStringLiteral("HH:mm")); onceDate_->setCalendarPopup(true); maxConcurrent_->setRange(1, 32); onComplete_->addItem(tr("Do nothing"), QStringLiteral("nothing")); onComplete_->addItem(tr("Exit Velox"), QStringLiteral("exit")); onComplete_->addItem(tr("Shut down"), QStringLiteral("shutdown")); onComplete_->addItem(tr("Hang up (disconnect network)"), QStringLiteral("hangup")); auto *daysRow = new QWidget(this); auto *daysLayout = new QHBoxLayout(daysRow); daysLayout->setContentsMargins(0, 0, 0, 0); for (int i = 0; i < 7; ++i) { dayChecks_[i] = new QCheckBox(tr(kDayLabels[i]), daysRow); daysLayout->addWidget(dayChecks_[i]); } auto *stopRow = new QWidget(this); auto *stopLayout = new QHBoxLayout(stopRow); stopLayout->setContentsMargins(0, 0, 0, 0); stopLayout->addWidget(stopTime_); stopLayout->addWidget(runUntilDrains_); connect(runUntilDrains_, &QCheckBox::toggled, stopTime_, &QWidget::setDisabled); const auto updateModeVisibility = [this, daysRow] { const bool periodic = mode_->currentData().toString() == QLatin1String("periodic"); daysRow->setVisible(periodic); onceDate_->setVisible(!periodic); }; connect(mode_, &QComboBox::currentIndexChanged, this, updateModeVisibility); updateModeVisibility(); form_ = new QWidget(this); auto *form = new QFormLayout(form_); form->addRow(useSchedule_); form->addRow(tr("Mode:"), mode_); form->addRow(tr("Start time:"), startTime_); form->addRow(tr("Stop time:"), stopRow); form->addRow(tr("Days:"), daysRow); form->addRow(tr("Date:"), onceDate_); form->addRow(tr("Max concurrent:"), maxConcurrent_); form->addRow(tr("When the queue finishes:"), onComplete_); connect(useSchedule_, &QCheckBox::toggled, this, [this](bool on) { for (QWidget *w : {static_cast(mode_), static_cast(startTime_), static_cast(stopTime_), static_cast(runUntilDrains_), static_cast(onceDate_)}) { w->setEnabled(on); } }); statusLabel_->setStyleSheet(theme::errorLabelStyle()); statusLabel_->hide(); form_->setEnabled(false); // no queue selected yet auto *startButton = new QPushButton(tr("Start Now"), this); auto *stopButton = new QPushButton(tr("Stop"), this); auto *saveButton = new QPushButton(tr("Save"), this); saveButton->setDefault(true); connect(startButton, &QPushButton::clicked, this, &SchedulerDialog::startNow); connect(stopButton, &QPushButton::clicked, this, &SchedulerDialog::stopNow); connect(saveButton, &QPushButton::clicked, this, &SchedulerDialog::save); auto *actionRow = new QHBoxLayout; actionRow->addWidget(startButton); actionRow->addWidget(stopButton); actionRow->addWidget(pauseRunningOnStop_); actionRow->addStretch(); actionRow->addWidget(saveButton); auto *closeButtons = new QDialogButtonBox(QDialogButtonBox::Close, this); connect(closeButtons, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(closeButtons, &QDialogButtonBox::accepted, this, &QDialog::accept); auto *rightLayout = new QVBoxLayout; rightLayout->addWidget(form_, 1); rightLayout->addWidget(statusLabel_); rightLayout->addLayout(actionRow); rightLayout->addWidget(closeButtons); auto *split = new QHBoxLayout(this); split->addWidget(queueList_); split->addLayout(rightLayout, 1); connect(queueList_, &QListWidget::currentItemChanged, this, &SchedulerDialog::onQueueSelected); QPointer self(this); client_->call(QString::fromLatin1(rpc::method::kQueueList), {}, [self](const rpc::RpcReply &reply) { if (!self || !reply.ok()) { return; } self->onQueuesLoaded(reply.result.toObject().value("items").toArray()); }); } void SchedulerDialog::onQueuesLoaded(const QJsonArray &items) { queues_ = items; queueList_->clear(); for (const QJsonValue &v : items) { auto *item = new QListWidgetItem(v.toObject().value("name").toString(), queueList_); item->setData(Qt::UserRole, v.toObject().value("queueId").toString()); } if (queueList_->count() > 0) { queueList_->setCurrentRow(0); } } QJsonObject SchedulerDialog::selectedQueue() const { if (!queueList_->currentItem()) { return {}; } const QString id = queueList_->currentItem()->data(Qt::UserRole).toString(); for (const QJsonValue &v : queues_) { if (v.toObject().value("queueId").toString() == id) { return v.toObject(); } } return {}; } void SchedulerDialog::onQueueSelected(QListWidgetItem *item, QListWidgetItem * /*previous*/) { if (!item) { form_->setEnabled(false); return; } populateForm(selectedQueue()); form_->setEnabled(true); } void SchedulerDialog::populateForm(const QJsonObject &queue) { maxConcurrent_->setValue(queue.value("maxConcurrent").toInt(1)); const int completeIdx = onComplete_->findData(queue.value("onComplete").toString("nothing")); onComplete_->setCurrentIndex(completeIdx >= 0 ? completeIdx : 0); const QJsonValue schedule = queue.value("schedule"); const bool has = schedule.isObject(); useSchedule_->setChecked(has); for (QWidget *w : {static_cast(mode_), static_cast(startTime_), static_cast(stopTime_), static_cast(runUntilDrains_), static_cast(onceDate_)}) { w->setEnabled(has); } if (!has) { return; } const QJsonObject sched = schedule.toObject(); const int modeIdx = mode_->findData(sched.value("mode").toString("once")); mode_->setCurrentIndex(modeIdx >= 0 ? modeIdx : 0); startTime_->setTime(QTime::fromString(sched.value("startTime").toString("00:00"), "HH:mm")); const bool stopIsNull = sched.value("stopTime").isNull(); runUntilDrains_->setChecked(stopIsNull); if (!stopIsNull) { stopTime_->setTime(QTime::fromString(sched.value("stopTime").toString(), "HH:mm")); } for (int i = 0; i < 7; ++i) { dayChecks_[i]->setChecked(false); } for (const QJsonValue &d : sched.value("daysOfWeek").toArray()) { const int idx = d.toInt(); if (idx >= 0 && idx < 7) { dayChecks_[idx]->setChecked(true); } } if (!sched.value("onceDate").isNull()) { onceDate_->setDate(QDate::fromString(sched.value("onceDate").toString(), Qt::ISODate)); } } void SchedulerDialog::save() { const QJsonObject queue = selectedQueue(); const QString queueId = queue.value("queueId").toString(); if (queueId.isEmpty()) { return; } QList days; for (int i = 0; i < 7; ++i) { if (dayChecks_[i]->isChecked()) { days << i; } } const QJsonValue schedule = buildSchedule( useSchedule_->isChecked(), mode_->currentData().toString(), startTime_->time().toString(QStringLiteral("HH:mm")), runUntilDrains_->isChecked(), stopTime_->time().toString(QStringLiteral("HH:mm")), days, onceDate_->date().toString(Qt::ISODate)); QPointer self(this); client_->call(QString::fromLatin1(rpc::method::kScheduleSet), QJsonObject{{"queueId", queueId}, {"schedule", schedule}}, [self, queueId](const rpc::RpcReply &reply) { if (!self) { return; } if (!reply.ok()) { self->statusLabel_->setText( tr("Could not save the schedule: %1").arg(reply.error.message)); self->statusLabel_->show(); return; } self->statusLabel_->hide(); }); // Queue-level fields (maxConcurrent/onComplete) go through queue.upsert; taskIds is // ignored by the daemon so re-sending the cached membership cannot drop a task. QJsonObject updatedQueue = queue; updatedQueue["maxConcurrent"] = maxConcurrent_->value(); updatedQueue["onComplete"] = onComplete_->currentData().toString(); client_->call(QString::fromLatin1(rpc::method::kQueueUpsert), QJsonObject{{"queue", updatedQueue}}, [self](const rpc::RpcReply &reply) { if (!self || !reply.ok()) { return; } const QJsonObject updated = reply.result.toObject().value("queue").toObject(); for (int i = 0; i < self->queues_.size(); ++i) { if (self->queues_[i].toObject().value("queueId").toString() == updated.value("queueId").toString()) { self->queues_[i] = updated; break; } } }); } void SchedulerDialog::startNow() { const QString queueId = selectedQueue().value("queueId").toString(); if (queueId.isEmpty()) { return; } client_->call(QString::fromLatin1(rpc::method::kQueueStart), QJsonObject{{"queueId", queueId}}); } void SchedulerDialog::stopNow() { const QString queueId = selectedQueue().value("queueId").toString(); if (queueId.isEmpty()) { return; } client_->call( QString::fromLatin1(rpc::method::kQueueStop), QJsonObject{{"queueId", queueId}, {"pauseRunning", pauseRunningOnStop_->isChecked()}}); } } // namespace velox::gui