Add services view, making it possible to stop engagements

This commit is contained in:
Pierre Ducroquet 2023-06-22 00:03:45 +02:00
parent 3692d52b6b
commit 7e19bc6e84
10 changed files with 406 additions and 30 deletions

View File

@ -32,6 +32,9 @@ set(PROJECT_SOURCES
ovhapi.cpp
ovhapi.h
resources.qrc
servicesview.h
servicesview.cpp
servicesview.ui
${TS_FILES}
)

View File

@ -204,135 +204,140 @@
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.ui" line="111"/>
<location filename="mainwindow.ui" line="112"/>
<source>&amp;Quit</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.ui" line="114"/>
<location filename="mainwindow.ui" line="115"/>
<source>Ctrl+Q</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.ui" line="131"/>
<location filename="mainwindow.ui" line="132"/>
<source>Graph per month</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.ui" line="136"/>
<location filename="mainwindow.ui" line="137"/>
<source>Bills of last month</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.ui" line="141"/>
<location filename="mainwindow.ui" line="142"/>
<source>Fidelity</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.ui" line="147"/>
<source>Services</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.ui" line="82"/>
<source>&amp;Login</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.ui" line="123"/>
<location filename="mainwindow.ui" line="124"/>
<source>&amp;New login</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.ui" line="126"/>
<location filename="mainwindow.ui" line="127"/>
<source>Ctrl+L</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="142"/>
<location filename="mainwindow.cpp" line="143"/>
<source>Validate credential</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="142"/>
<location filename="mainwindow.cpp" line="143"/>
<source>Have you validated the OVH credential request?</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="184"/>
<location filename="mainwindow.cpp" line="185"/>
<source>Info %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="199"/>
<location filename="mainwindow.cpp" line="200"/>
<source>IPMI not activated</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="199"/>
<location filename="mainwindow.cpp" line="200"/>
<source>IPMI is not activated on this server</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="208"/>
<location filename="mainwindow.cpp" line="209"/>
<source>IPMI KVM not available</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="208"/>
<location filename="mainwindow.cpp" line="209"/>
<source>IPMI KVM is not available on this server</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="232"/>
<location filename="mainwindow.cpp" line="233"/>
<source>Waiting for task... %1</source>
<oldsource>Waiting for task...</oldsource>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="252"/>
<location filename="mainwindow.cpp" line="253"/>
<source>KVM %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="261"/>
<location filename="mainwindow.cpp" line="263"/>
<location filename="mainwindow.cpp" line="262"/>
<location filename="mainwindow.cpp" line="264"/>
<source>JNLP KVM</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="261"/>
<location filename="mainwindow.cpp" line="262"/>
<source>This server only supports a Java WebStart KVM, it will thus not be embedded in this window.
Java WebStart should be starting right now, wait until it finishes loading to close this dialog.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="263"/>
<location filename="mainwindow.cpp" line="264"/>
<source>Failed to launch javaws. Make sure it is properly installed.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="362"/>
<location filename="mainwindow.cpp" line="363"/>
<source>Target archive</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="362"/>
<location filename="mainwindow.cpp" line="363"/>
<source>Zip files (*.zip)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="366"/>
<location filename="mainwindow.cpp" line="367"/>
<source>OVH...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="366"/>
<location filename="mainwindow.cpp" line="367"/>
<source>OVH decided that getting a bill require a &apos;true&apos; login. I&apos;m going to show you a web browser for that.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="377"/>
<location filename="mainwindow.cpp" line="378"/>
<source>Failed to open archive</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwindow.cpp" line="377"/>
<location filename="mainwindow.cpp" line="378"/>
<source>Failed to open archive...</source>
<translation type="unfinished"></translation>
</message>
@ -350,4 +355,42 @@ Java WebStart should be starting right now, wait until it finishes loading to cl
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ServicesView</name>
<message>
<location filename="servicesview.ui" line="14"/>
<source>Services</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="servicesview.ui" line="30"/>
<source>ID</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="servicesview.ui" line="35"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="servicesview.ui" line="40"/>
<source>Creation</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="servicesview.ui" line="45"/>
<source>Expiration</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="servicesview.ui" line="50"/>
<source>Engagement</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="servicesview.ui" line="55"/>
<source>Strategy</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View File

@ -23,6 +23,7 @@
#include <QFileDialog>
#include <KArchive>
#include <KZip>
#include "servicesview.h"
#include "dedicatedserverinfowidget.h"
#include "ovhwebauthentication.h"
@ -424,3 +425,22 @@ QCoro::Task<> MainWindow::on_actionBillsOfLastMonth_triggered()
QDesktopServices::openUrl(QUrl::fromLocalFile(fileName));
}
QCoro::Task<> MainWindow::on_actionFidelity_triggered()
{
QString path = QString("/me/fidelityAccount/movements?date.from=2012-11-21");
auto movementList = (co_await api->get(path)).array();
for (const auto &&movementId: movementList) {
QString movementPath = QString("/me/fidelityAccount/movements/%1").arg(movementId.toInt());
auto movement = (co_await api->get(movementPath, ten_years)).object();
qDebug() << movement;
}
}
void MainWindow::on_actionServices_triggered()
{
auto view = new ServicesView(api, this);
view->exec();
}

View File

@ -42,6 +42,10 @@ private slots:
QCoro::Task<> on_actionBillsOfLastMonth_triggered();
QCoro::Task<> on_actionFidelity_triggered();
void on_actionServices_triggered();
private:
QAction *addLoginAction(const QString &ck, const QString &display);
QMap<QString, QTreeWidgetItem*> topLevelNodes;

View File

@ -96,6 +96,8 @@
</property>
<addaction name="actionGraphPerMonth"/>
<addaction name="actionBillsOfLastMonth"/>
<addaction name="actionFidelity"/>
<addaction name="actionServices"/>
</widget>
<addaction name="menuOvh_KVM"/>
<addaction name="menuBilling"/>
@ -135,6 +137,16 @@
<string>Bills of last month</string>
</property>
</action>
<action name="actionFidelity">
<property name="text">
<string>Fidelity</string>
</property>
</action>
<action name="actionServices">
<property name="text">
<string>Services</string>
</property>
</action>
</widget>
<resources>
<include location="resources.qrc"/>

View File

@ -18,6 +18,7 @@ static const char* APPSECRET = "a81709def024dc175b82d6a87762bebf";
static const int initialOffset = 0x12345678;
OvhApi::OvhApi(const QString &baseUrl, QObject *parent)
: QObject{parent}, baseUrl{baseUrl}
{
@ -78,9 +79,9 @@ QCoro::Task<QByteArray> OvhApi::download(const QUrl &url)
co_return rawAnswer;
}
QCoro::Task<QJsonDocument> OvhApi::get(const QString &path, std::chrono::seconds cacheDuration)
QCoro::Task<QJsonDocument> OvhApi::get(const QString &path, std::chrono::seconds cacheDuration, bool forceRefresh)
{
if (cacheDuration.count() > 0) {
if (cacheDuration.count() > 0 && !forceRefresh) {
qDebug() << "Gonna look at the cache";
QSqlDatabase db = QSqlDatabase::database();
if (db.isValid()) {
@ -108,7 +109,7 @@ QCoro::Task<QJsonDocument> OvhApi::get(const QString &path, std::chrono::seconds
QByteArray rawAnswer = reply->readAll();
auto answer = QJsonDocument::fromJson(rawAnswer);
if (cacheDuration.count() > 0) {
if (cacheDuration.count() > 0 || forceRefresh) {
qDebug() << "Gonna store in the cache";
QSqlDatabase db = QSqlDatabase::database();
if (db.isValid()) {

View File

@ -17,7 +17,7 @@ public:
explicit OvhApi(const QString &baseUrl, QObject *parent = nullptr);
void setConsumerKey(const QString &consumerKey);
public slots:
QCoro::Task<QJsonDocument> get(const QString &path, std::chrono::seconds cacheDuration = 0s);
QCoro::Task<QJsonDocument> get(const QString &path, std::chrono::seconds cacheDuration = 0s, bool forceRefresh = false);
QCoro::Task<QJsonDocument> post(const QString &path, const QByteArray &data);
QCoro::Task<QJsonDocument> put(const QString &path, const QByteArray &data);
QCoro::Task<> deleteResource(const QString &path);

152
servicesview.cpp Normal file
View File

@ -0,0 +1,152 @@
#include "servicesview.h"
#include "ui_servicesview.h"
#include "ovhapi.h"
#include <QDebug>
#include <QTimer>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPushButton>
#include <QComboBox>
#include <QLocale>
using namespace std::chrono_literals;
ServicesView::ServicesView(OvhApi *api, QWidget *parent) :
QDialog(parent),
ui(new Ui::ServicesView),
api(api)
{
ui->setupUi(this);
ui->tableWidget->setColumnHidden(0, true);
ui->tableWidget->setColumnWidth(1, 250);
ui->tableWidget->setColumnWidth(5, 250);
ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
// Load services in the event loop, "when possible"
QTimer::singleShot(0, this, &ServicesView::loadServices);
}
ServicesView::~ServicesView()
{
delete ui;
}
QCoro::Task<> ServicesView::loadServices()
{
ui->tableWidget->disconnect(SIGNAL(cellChanged(int, int)));
auto serviceIds = (co_await api->get("/services")).array();
int rowCount = serviceIds.count();
ui->tableWidget->setRowCount(serviceIds.count());
QLocale locale;
int i = 0;
for (const auto &&serviceIdJson: serviceIds) {
int serviceId = serviceIdJson.toInt();
auto serviceInfo = (co_await api->get(QString("/services/%1").arg(serviceId), 1h)).object();
qDebug() << serviceInfo;
if (!serviceInfo["parentServiceId"].isNull()) {
rowCount--;
continue;
}
auto resourceInfo = serviceInfo["resource"].toObject();
auto billingInfo = serviceInfo["billing"].toObject();
auto currentLifecycle = billingInfo["lifecycle"].toObject()["current"].toObject();
auto engagement = billingInfo["engagement"].toObject();
int c = 0;
auto tableItem = new QTableWidgetItem(QString::number(serviceId));
tableItem->setFlags(Qt::ItemIsEnabled);
ui->tableWidget->setItem(i, c++, tableItem);
tableItem = new QTableWidgetItem(resourceInfo["displayName"].toString());
ui->tableWidget->setItem(i, c++, tableItem);
auto creationDate = QDateTime::fromString(currentLifecycle["creationDate"].toString(), Qt::ISODate);
tableItem = new QTableWidgetItem(locale.toString(creationDate.date(), QLocale::ShortFormat));
tableItem->setFlags(Qt::ItemIsEnabled);
ui->tableWidget->setItem(i, c++, tableItem);
auto expirationDate = QDateTime::fromString(billingInfo["expirationDate"].toString(), Qt::ISODate);
tableItem = new QTableWidgetItem(locale.toString(expirationDate.date(), QLocale::ShortFormat));
tableItem->setFlags(Qt::ItemIsEnabled);
ui->tableWidget->setItem(i, c++, tableItem);
if (!billingInfo["engagement"].isNull()) {
auto endDate = QDate::fromString(engagement["endDate"].toString(), Qt::ISODate);
tableItem = new QTableWidgetItem(locale.toString(endDate, QLocale::ShortFormat));
tableItem->setFlags(Qt::ItemIsEnabled);
ui->tableWidget->setItem(i, c++, tableItem);
auto endRule = engagement["endRule"].toObject();
tableItem = new QTableWidgetItem(endRule["strategy"].toString());
tableItem->setFlags(Qt::ItemIsEnabled);// TODO : editable with option list
ui->tableWidget->setItem(i, c, tableItem);
auto strategyCombobox = new QComboBox();
QStringList strategies;
for (const auto &&possibleStrategy: endRule["possibleStrategies"].toArray())
strategies.append(possibleStrategy.toString());
if (!strategies.contains(endRule["strategy"].toString()))
strategies.append(endRule["strategy"].toString());
strategyCombobox->addItems(strategies);
strategyCombobox->setCurrentIndex(strategies.indexOf(endRule["strategy"].toString()));
// Can't use the row id here, sorting will break it.
connect(strategyCombobox, &QComboBox::currentTextChanged, this, [this, serviceId] (const QString &newStrategy) {
qDebug() << "setting text to " << newStrategy << " for service " << serviceId;
// Find it back...
for (int r = 0 ; r < ui->tableWidget->rowCount() ; r++) {
if (ui->tableWidget->item(r, 0)->text().toInt() == serviceId) {
ui->tableWidget->item(r, 5)->setText(newStrategy);
}
}
});
ui->tableWidget->setCellWidget(i, c, strategyCombobox);
}
i++;
}
ui->tableWidget->setRowCount(rowCount);
connect(ui->tableWidget, &QTableWidget::cellChanged, this, &ServicesView::on_tableWidget_cellChanged);
}
void ServicesView::on_tableWidget_cellChanged(int row, [[maybe_unused]] int column)
{
int serviceId = ui->tableWidget->item(row, 0)->text().toInt();
if (!changedServices.contains(serviceId))
changedServices.append(serviceId);
qDebug() << "changed:" << changedServices;
ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
}
QCoro::Task<> ServicesView::on_buttonBox_clicked(QAbstractButton *button)
{
if (button == ui->buttonBox->button(QDialogButtonBox::Apply)) {
qDebug() << "APPLY for " << changedServices;
this->setEnabled(false);
for (int r = 0 ; r < ui->tableWidget->rowCount() ; r++) {
int serviceId = ui->tableWidget->item(r, 0)->text().toInt();
if (changedServices.contains(serviceId)) {
qDebug() << "Need to send changed values at row " << r;
QString newDisplayName = ui->tableWidget->item(r, 1)->text();
QString newStrategy = ui->tableWidget->item(r, 5)->text();
qDebug() << "New name ? " << newDisplayName;
qDebug() << "New strategy ? " << newStrategy;
auto currentInfo = (co_await api->get(QString("/services/%1").arg(serviceId))).object();
bool needToRefreshCache = false;
if (currentInfo["resource"].toObject()["displayName"].toString() != newDisplayName) {
// Build a PUT.
QJsonObject data {
{"displayName", newDisplayName}
};
co_await api->put(QString("/services/%1").arg(serviceId), QJsonDocument(data).toJson());
needToRefreshCache = true;
}
auto currentEngagement = currentInfo["billing"].toObject()["engagement"].toObject();
if (currentEngagement["endRule"].toObject()["strategy"] != newStrategy) {
QJsonObject data {
{"strategy", newStrategy}
};
co_await api->put(QString("/services/%1/billing/engagement/endRule").arg(serviceId), QJsonDocument(data).toJson());
needToRefreshCache = true;
}
if (needToRefreshCache)
co_await api->get(QString("/services/%1").arg(serviceId), 1h, true);
}
}
this->setEnabled(true);
this->accept();
}
}

34
servicesview.h Normal file
View File

@ -0,0 +1,34 @@
#ifndef SERVICESVIEW_H
#define SERVICESVIEW_H
#include <QDialog>
#include <QCoroTask>
class OvhApi;
class QAbstractButton;
namespace Ui {
class ServicesView;
}
class ServicesView : public QDialog
{
Q_OBJECT
public:
explicit ServicesView(OvhApi *api, QWidget *parent = nullptr);
~ServicesView();
private slots:
QCoro::Task<> loadServices();
void on_tableWidget_cellChanged(int row, int column);
QCoro::Task<> on_buttonBox_clicked(QAbstractButton *button);
private:
Ui::ServicesView *ui;
OvhApi *api;
QList <int> changedServices;
};
#endif // SERVICESVIEW_H

107
servicesview.ui Normal file
View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ServicesView</class>
<widget class="QDialog" name="ServicesView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>652</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Services</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTableWidget" name="tableWidget">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>ID</string>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Creation</string>
</property>
</column>
<column>
<property name="text">
<string>Expiration</string>
</property>
</column>
<column>
<property name="text">
<string>Engagement</string>
</property>
</column>
<column>
<property name="text">
<string>Strategy</string>
</property>
</column>
</widget>
</item>
<item row="1" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Apply|QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ServicesView</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ServicesView</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>