Merge axivion plugin into 11.0

Change-Id: I60290479f6b4bd8ff87e86f0cc6ded240803d43c
This commit is contained in:
Eike Ziller
2023-05-24 11:22:26 +02:00
21 changed files with 2024 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
{
\"Name\" : \"Axivion\",
\"Version\" : \"$$QTCREATOR_VERSION\",
\"CompatVersion\" : \"$$QTCREATOR_COMPAT_VERSION\",
\"Revision\" : \"$$QTC_PLUGIN_REVISION\",
\"Experimental\" : true,
\"Vendor\" : \"The Qt Company Ltd\",
\"Copyright\" : \"(C) $$QTCREATOR_COPYRIGHT_YEAR The Qt Company Ltd\",
\"License\" : [ \"Commercial Usage\",
\"\",
\"Licensees holding valid Qt Enterprise licenses may use this plugin in accordance with the Qt Enterprise License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and The Qt Company.\"
],
\"Category\" : \"Code Analyzer\",
\"Description\" : \"Integration of the axivion dashboard.\",
\"Url\" : \"http://www.qt-project.org\",
$$dependencyList
}

View File

@@ -0,0 +1,24 @@
find_package(QtCreator COMPONENTS Core REQUIRED)
find_package(Qt6 COMPONENTS Network Widgets REQUIRED)
find_package(QtCreatorLicenseChecker QUIET)
if (TARGET QtCreator::LicenseChecker)
set(LICENSECHECKER_DEPENDS QtCreator::LicenseChecker)
endif()
add_qtc_plugin(Axivion
PLUGIN_DEPENDS
QtCreator::Core QtCreator::ProjectExplorer QtCreator::TextEditor
${LICENSECHECKER_DEPENDS}
DEPENDS Qt::Network Qt::Widgets QtCreator::ExtensionSystem QtCreator::Utils
SOURCES
axivion.qrc
axivionoutputpane.cpp axivionoutputpane.h
axivionplugin.cpp axivionplugin.h
axivionprojectsettings.h axivionprojectsettings.cpp
axivionquery.h axivionquery.cpp
axivionresultparser.h axivionresultparser.cpp
axivionsettings.cpp axivionsettings.h
axivionsettingspage.cpp axivionsettingspage.h
axiviontr.h
)

View File

@@ -0,0 +1,32 @@
import qbs
QtcCommercialPlugin {
name: "Axivion"
Depends { name: "Core" }
Depends { name: "ProjectExplorer" }
Depends { name: "TextEditor" }
Depends { name: "ExtensionSystem" }
Depends { name: "Utils" }
Depends { name: "Qt.widgets" }
Depends { name: "Qt.network" }
files: [
"axivion.qrc",
"axivionoutputpane.cpp",
"axivionoutputpane.h",
"axivionplugin.cpp",
"axivionplugin.h",
"axivionprojectsettings.h",
"axivionprojectsettings.cpp",
"axivionquery.h",
"axivionquery.cpp",
"axivionresultparser.h",
"axivionresultparser.cpp",
"axivionsettings.cpp",
"axivionsettings.h",
"axivionsettingspage.cpp",
"axivionsettingspage.h",
"axiviontr.h",
]
}

View File

@@ -0,0 +1,6 @@
<RCC>
<qresource prefix="/axivion">
<file>images/axivion.png</file>
<file>images/axivion@2x.png</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,209 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#include "axivionoutputpane.h"
#include "axivionplugin.h"
#include "axivionresultparser.h"
#include "axiviontr.h"
#include <utils/qtcassert.h>
#include <utils/utilsicons.h>
#include <QFormLayout>
#include <QLabel>
#include <QScrollArea>
#include <QStackedWidget>
#include <QTextBrowser>
#include <QToolButton>
namespace Axivion::Internal {
class DashboardWidget : public QScrollArea
{
public:
explicit DashboardWidget(QWidget *parent = nullptr);
void updateUi();
bool hasProject() const { return !m_project->text().isEmpty(); }
private:
QLabel *m_project = nullptr;
QLabel *m_loc = nullptr;
QFormLayout *m_formLayout = nullptr;
};
DashboardWidget::DashboardWidget(QWidget *parent)
: QScrollArea(parent)
{
QWidget *widget = new QWidget(this);
QVBoxLayout *layout = new QVBoxLayout(widget);
QFormLayout *projectLayout = new QFormLayout;
m_project = new QLabel(this);
projectLayout->addRow(Tr::tr("Project:"), m_project);
m_loc = new QLabel(this);
projectLayout->addRow(Tr::tr("Lines of Code:"), m_loc);
layout->addLayout(projectLayout);
m_formLayout = new QFormLayout;
layout->addLayout(m_formLayout);
setWidget(widget);
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
setWidgetResizable(true);
}
void DashboardWidget::updateUi()
{
const ProjectInfo &info = AxivionPlugin::projectInfo();
m_project->setText(info.name);
m_loc->setText({});
while (m_formLayout->rowCount())
m_formLayout->removeRow(0);
if (info.versions.isEmpty())
return;
const ResultVersion &last = info.versions.last();
m_loc->setText(QString::number(last.linesOfCode));
const QString tmpl("%1 %2 +%3 / -%4");
auto apply = [&tmpl](int t, int a, int r){
QChar tr = (a == r ? '=' : (a < r ? '^' : 'v'));
return tmpl.arg(t, 10, 10, QLatin1Char(' ')).arg(tr).arg(a, 5, 10, QLatin1Char(' '))
.arg(r, 5, 10, QLatin1Char(' '));
};
const QList<IssueKind> &issueKinds = info.issueKinds;
auto toolTip = [issueKinds](const QString &prefix){
for (const IssueKind &kind : issueKinds) {
if (kind.prefix == prefix)
return kind.nicePlural;
}
return QString();
};
int allTotal = 0, allAdded = 0, allRemoved = 0;
for (auto issueCount : std::as_const(last.issueCounts)) {
allTotal += issueCount.total;
allAdded += issueCount.added;
allRemoved += issueCount.removed;
const QString txt = apply(issueCount.total, issueCount.added, issueCount.removed);
const QString currentToolTip = toolTip(issueCount.issueKind);
QLabel *label = new QLabel(issueCount.issueKind, this);
label->setToolTip(currentToolTip);
QLabel *values = new QLabel(txt, this);
values->setToolTip(currentToolTip);
m_formLayout->addRow(label, values);
}
QLabel *label = new QLabel(apply(allTotal, allAdded, allRemoved), this);
m_formLayout->addRow(Tr::tr("Total:"), label);
}
AxivionOutputPane::AxivionOutputPane(QObject *parent)
: Core::IOutputPane(parent)
{
m_outputWidget = new QStackedWidget;
DashboardWidget *dashboardWidget = new DashboardWidget(m_outputWidget);
m_outputWidget->addWidget(dashboardWidget);
QTextBrowser *browser = new QTextBrowser(m_outputWidget);
m_outputWidget->addWidget(browser);
}
AxivionOutputPane::~AxivionOutputPane()
{
if (!m_outputWidget->parent())
delete m_outputWidget;
}
QWidget *AxivionOutputPane::outputWidget(QWidget *parent)
{
if (m_outputWidget)
m_outputWidget->setParent(parent);
else
QTC_CHECK(false);
return m_outputWidget;
}
QList<QWidget *> AxivionOutputPane::toolBarWidgets() const
{
QList<QWidget *> buttons;
auto showDashboard = new QToolButton(m_outputWidget);
showDashboard->setIcon(Utils::Icons::ONLINE_TOOLBAR.icon());
showDashboard->setToolTip(Tr::tr("Show dashboard"));
connect(showDashboard, &QToolButton::clicked, this, [this]{
QTC_ASSERT(m_outputWidget, return);
m_outputWidget->setCurrentIndex(0);
});
buttons.append(showDashboard);
return buttons;
}
QString AxivionOutputPane::displayName() const
{
return Tr::tr("Axivion");
}
int AxivionOutputPane::priorityInStatusBar() const
{
return -1;
}
void AxivionOutputPane::clearContents()
{
}
void AxivionOutputPane::setFocus()
{
}
bool AxivionOutputPane::hasFocus() const
{
return false;
}
bool AxivionOutputPane::canFocus() const
{
return true;
}
bool AxivionOutputPane::canNavigate() const
{
return true;
}
bool AxivionOutputPane::canNext() const
{
return false;
}
bool AxivionOutputPane::canPrevious() const
{
return false;
}
void AxivionOutputPane::goToNext()
{
}
void AxivionOutputPane::goToPrev()
{
}
void AxivionOutputPane::updateDashboard()
{
if (auto dashboard = static_cast<DashboardWidget *>(m_outputWidget->widget(0))) {
dashboard->updateUi();
m_outputWidget->setCurrentIndex(0);
if (dashboard->hasProject())
flash();
}
}
void AxivionOutputPane::updateAndShowRule(const QString &ruleHtml)
{
if (auto browser = static_cast<QTextBrowser *>(m_outputWidget->widget(1))) {
browser->setText(ruleHtml);
if (!ruleHtml.isEmpty()) {
m_outputWidget->setCurrentIndex(1);
popup(Core::IOutputPane::NoModeSwitch);
}
}
}
} // Axivion::Internal

View File

@@ -0,0 +1,42 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#pragma once
#include <coreplugin/ioutputpane.h>
QT_BEGIN_NAMESPACE
class QStackedWidget;
QT_END_NAMESPACE
namespace Axivion::Internal {
class AxivionOutputPane : public Core::IOutputPane
{
Q_OBJECT
public:
explicit AxivionOutputPane(QObject *parent = nullptr);
~AxivionOutputPane();
// IOutputPane interface
QWidget *outputWidget(QWidget *parent) override;
QList<QWidget *> toolBarWidgets() const override;
QString displayName() const override;
int priorityInStatusBar() const override;
void clearContents() override;
void setFocus() override;
bool hasFocus() const override;
bool canFocus() const override;
bool canNavigate() const override;
bool canNext() const override;
bool canPrevious() const override;
void goToNext() override;
void goToPrev() override;
void updateDashboard();
void updateAndShowRule(const QString &ruleHtml);
private:
QStackedWidget *m_outputWidget = nullptr;
};
} // Axivion::Internal

View File

@@ -0,0 +1,360 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#include "axivionplugin.h"
#include "axivionoutputpane.h"
#include "axivionprojectsettings.h"
#include "axivionquery.h"
#include "axivionresultparser.h"
#include "axivionsettings.h"
#include "axivionsettingspage.h"
#include "axiviontr.h"
#include <coreplugin/editormanager/documentmodel.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h>
#include <extensionsystem/pluginmanager.h>
#include <projectexplorer/buildsystem.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projectpanelfactory.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <texteditor/textmark.h>
#include <utils/qtcassert.h>
#include <utils/utilsicons.h>
#ifdef LICENSECHECKER
# include <licensechecker/licensecheckerplugin.h>
#endif
#include <QAction>
#include <QMessageBox>
#include <QTimer>
constexpr char AxivionTextMarkId[] = "AxivionTextMark";
namespace Axivion::Internal {
class AxivionPluginPrivate : public QObject
{
public:
AxivionProjectSettings *projectSettings(ProjectExplorer::Project *project);
void onStartupProjectChanged();
void fetchProjectInfo(const QString &projectName);
void handleProjectInfo(const ProjectInfo &info);
void handleOpenedDocs(ProjectExplorer::Project *project);
void onDocumentOpened(Core::IDocument *doc);
void onDocumentClosed(Core::IDocument * doc);
void clearAllMarks();
void handleIssuesForFile(const IssuesList &issues);
void fetchRuleInfo(const QString &id);
AxivionSettings m_axivionSettings;
AxivionSettingsPage m_axivionSettingsPage{&m_axivionSettings};
AxivionOutputPane m_axivionOutputPane;
QHash<ProjectExplorer::Project *, AxivionProjectSettings *> m_axivionProjectSettings;
ProjectInfo m_currentProjectInfo;
bool m_runningQuery = false;
};
static AxivionPlugin *s_instance = nullptr;
static AxivionPluginPrivate *dd = nullptr;
class AxivionTextMark : public TextEditor::TextMark
{
public:
AxivionTextMark(const Utils::FilePath &filePath, const ShortIssue &issue);
private:
QString m_id;
};
AxivionTextMark::AxivionTextMark(const Utils::FilePath &filePath, const ShortIssue &issue)
: TextEditor::TextMark(filePath, issue.lineNumber, {Tr::tr("Axivion"), AxivionTextMarkId})
, m_id(issue.id)
{
const QString markText = issue.entity.isEmpty() ? issue.message
: issue.entity + ": " + issue.message;
setToolTip(issue.errorNumber + " " + markText);
setPriority(TextEditor::TextMark::NormalPriority);
setLineAnnotation(markText);
setActionsProvider([this]{
auto action = new QAction;
action->setIcon(Utils::Icons::INFO.icon());
action->setToolTip(Tr::tr("Show rule details"));
QObject::connect(action, &QAction::triggered,
dd, [this]{ dd->fetchRuleInfo(m_id); });
return QList{action};
});
}
AxivionPlugin::AxivionPlugin()
{
s_instance = this;
}
AxivionPlugin::~AxivionPlugin()
{
if (dd && !dd->m_axivionProjectSettings.isEmpty()) {
qDeleteAll(dd->m_axivionProjectSettings);
dd->m_axivionProjectSettings.clear();
}
delete dd;
dd = nullptr;
}
AxivionPlugin *AxivionPlugin::instance()
{
return s_instance;
}
bool AxivionPlugin::initialize(const QStringList &arguments, QString *errorMessage)
{
Q_UNUSED(arguments)
Q_UNUSED(errorMessage)
#ifdef LICENSECHECKER
LicenseChecker::LicenseCheckerPlugin *licenseChecker
= ExtensionSystem::PluginManager::getObject<LicenseChecker::LicenseCheckerPlugin>();
if (!licenseChecker || !licenseChecker->hasValidLicense() || !licenseChecker->enterpriseFeatures())
return true;
#endif // LICENSECHECKER
dd = new AxivionPluginPrivate;
dd->m_axivionSettings.fromSettings(Core::ICore::settings());
auto panelFactory = new ProjectExplorer::ProjectPanelFactory;
panelFactory->setPriority(250);
panelFactory->setDisplayName(Tr::tr("Axivion"));
panelFactory->setCreateWidgetFunction([](ProjectExplorer::Project *project){
return new AxivionProjectSettingsWidget(project);
});
ProjectExplorer::ProjectPanelFactory::registerFactory(panelFactory);
connect(ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::startupProjectChanged,
dd, &AxivionPluginPrivate::onStartupProjectChanged);
connect(Core::EditorManager::instance(), &Core::EditorManager::documentOpened,
dd, &AxivionPluginPrivate::onDocumentOpened);
connect(Core::EditorManager::instance(), &Core::EditorManager::documentClosed,
dd, &AxivionPluginPrivate::onDocumentClosed);
return true;
}
AxivionSettings *AxivionPlugin::settings()
{
QTC_ASSERT(dd, return nullptr);
return &dd->m_axivionSettings;
}
AxivionProjectSettings *AxivionPlugin::projectSettings(ProjectExplorer::Project *project)
{
QTC_ASSERT(project, return nullptr);
QTC_ASSERT(dd, return nullptr);
return dd->projectSettings(project);
}
bool AxivionPlugin::handleCertificateIssue()
{
QTC_ASSERT(dd, return false);
const QString serverHost = QUrl(dd->m_axivionSettings.server.dashboard).host();
if (QMessageBox::question(Core::ICore::dialogParent(), Tr::tr("Certificate Error"),
Tr::tr("Server certificate for %1 cannot be authenticated.\n"
"Do you want to disable SSL verification for this server?\n"
"Note: This can expose you to man-in-the-middle attack.")
.arg(serverHost))
!= QMessageBox::Yes) {
return false;
}
dd->m_axivionSettings.server.validateCert = false;
emit s_instance->settingsChanged();
return true;
}
void AxivionPlugin::fetchProjectInfo(const QString &projectName)
{
QTC_ASSERT(dd, return);
dd->fetchProjectInfo(projectName);
}
ProjectInfo AxivionPlugin::projectInfo()
{
QTC_ASSERT(dd, return {});
return dd->m_currentProjectInfo;
}
AxivionProjectSettings *AxivionPluginPrivate::projectSettings(ProjectExplorer::Project *project)
{
auto &settings = m_axivionProjectSettings[project];
if (!settings)
settings = new AxivionProjectSettings(project);
return settings;
}
void AxivionPluginPrivate::onStartupProjectChanged()
{
ProjectExplorer::Project *project = ProjectExplorer::ProjectManager::startupProject();
if (!project) {
clearAllMarks();
m_currentProjectInfo = ProjectInfo();
m_axivionOutputPane.updateDashboard();
return;
}
const AxivionProjectSettings *projSettings = projectSettings(project);
fetchProjectInfo(projSettings->dashboardProjectName());
}
void AxivionPluginPrivate::fetchProjectInfo(const QString &projectName)
{
if (m_runningQuery) { // re-schedule
QTimer::singleShot(3000, [this, projectName]{ fetchProjectInfo(projectName); });
return;
}
clearAllMarks();
if (projectName.isEmpty()) {
m_currentProjectInfo = ProjectInfo();
m_axivionOutputPane.updateDashboard();
return;
}
m_runningQuery = true;
AxivionQuery query(AxivionQuery::ProjectInfo, {projectName});
AxivionQueryRunner *runner = new AxivionQueryRunner(query, this);
connect(runner, &AxivionQueryRunner::resultRetrieved, this, [this](const QByteArray &result){
handleProjectInfo(ResultParser::parseProjectInfo(result));
});
connect(runner, &AxivionQueryRunner::finished, [runner]{ runner->deleteLater(); });
runner->start();
}
void AxivionPluginPrivate::fetchRuleInfo(const QString &id)
{
if (m_runningQuery) {
QTimer::singleShot(3000, [this, id]{ fetchRuleInfo(id); });
return;
}
const QStringList args = id.split(':');
QTC_ASSERT(args.size() == 2, return);
m_runningQuery = true;
AxivionQuery query(AxivionQuery::RuleInfo, args);
AxivionQueryRunner *runner = new AxivionQueryRunner(query, this);
connect(runner, &AxivionQueryRunner::resultRetrieved, this, [this](const QByteArray &result){
m_runningQuery = false;
m_axivionOutputPane.updateAndShowRule(ResultParser::parseRuleInfo(result));
});
connect(runner, &AxivionQueryRunner::finished, [runner]{ runner->deleteLater(); });
runner->start();
}
void AxivionPluginPrivate::handleOpenedDocs(ProjectExplorer::Project *project)
{
if (project && ProjectExplorer::ProjectManager::startupProject() != project)
return;
const QList<Core::IDocument *> openDocuments = Core::DocumentModel::openedDocuments();
for (Core::IDocument *doc : openDocuments)
onDocumentOpened(doc);
if (project)
disconnect(ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectFinishedParsing,
this, &AxivionPluginPrivate::handleOpenedDocs);
}
void AxivionPluginPrivate::clearAllMarks()
{
const QList<Core::IDocument *> openDocuments = Core::DocumentModel::openedDocuments();
for (Core::IDocument *doc : openDocuments)
onDocumentClosed(doc);
}
void AxivionPluginPrivate::handleProjectInfo(const ProjectInfo &info)
{
m_runningQuery = false;
if (!info.error.isEmpty()) {
Core::MessageManager::writeFlashing("Axivion: " + info.error);
return;
}
m_currentProjectInfo = info;
m_axivionOutputPane.updateDashboard();
if (m_currentProjectInfo.name.isEmpty())
return;
// handle already opened documents
if (auto buildSystem = ProjectExplorer::ProjectManager::startupBuildSystem();
!buildSystem || !buildSystem->isParsing()) {
handleOpenedDocs(nullptr);
} else {
connect(ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectFinishedParsing,
this, &AxivionPluginPrivate::handleOpenedDocs);
}
}
void AxivionPluginPrivate::onDocumentOpened(Core::IDocument *doc)
{
if (m_currentProjectInfo.name.isEmpty()) // we do not have a project info (yet)
return;
ProjectExplorer::Project *project = ProjectExplorer::ProjectManager::startupProject();
if (!doc || !project->isKnownFile(doc->filePath()))
return;
Utils::FilePath relative = doc->filePath().relativeChildPath(project->projectDirectory());
// for now only style violations
AxivionQuery query(AxivionQuery::IssuesForFileList, {m_currentProjectInfo.name, "SV",
relative.path() } );
AxivionQueryRunner *runner = new AxivionQueryRunner(query, this);
connect(runner, &AxivionQueryRunner::resultRetrieved, this, [this](const QByteArray &result){
handleIssuesForFile(ResultParser::parseIssuesList(result));
});
connect(runner, &AxivionQueryRunner::finished, [runner]{ runner->deleteLater(); });
runner->start();
}
void AxivionPluginPrivate::onDocumentClosed(Core::IDocument *doc)
{
const auto document = qobject_cast<TextEditor::TextDocument *>(doc);
if (!document)
return;
const TextEditor::TextMarks marks = document->marks();
for (auto m : marks) {
if (m->category().id == AxivionTextMarkId)
delete m;
}
}
void AxivionPluginPrivate::handleIssuesForFile(const IssuesList &issues)
{
if (issues.issues.isEmpty())
return;
ProjectExplorer::Project *project = ProjectExplorer::ProjectManager::startupProject();
if (!project)
return;
const Utils::FilePath filePath = project->projectDirectory()
.pathAppended(issues.issues.first().filePath);
const Utils::Id axivionId(AxivionTextMarkId);
for (const ShortIssue &issue : std::as_const(issues.issues)) {
// FIXME the line location can be wrong (even the whole issue could be wrong)
// depending on whether this line has been changed since the last axivion run and the
// current state of the file - some magic has to happen here
new AxivionTextMark(filePath, issue);
}
}
} // Axivion::Internal

View File

@@ -0,0 +1,41 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#pragma once
#include <extensionsystem/iplugin.h>
namespace ProjectExplorer { class Project; }
namespace Axivion::Internal {
class AxivionSettings;
class AxivionProjectSettings;
class ProjectInfo;
class AxivionPlugin final : public ExtensionSystem::IPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Axivion.json")
public:
AxivionPlugin();
~AxivionPlugin() final;
static AxivionPlugin *instance();
static AxivionSettings *settings();
static AxivionProjectSettings *projectSettings(ProjectExplorer::Project *project);
static bool handleCertificateIssue();
static void fetchProjectInfo(const QString &projectName);
static ProjectInfo projectInfo();
signals:
void settingsChanged();
private:
bool initialize(const QStringList &arguments, QString *errorMessage) final;
void extensionsInitialized() final {}
};
} // Axivion::Internal

View File

@@ -0,0 +1,184 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#include "axivionprojectsettings.h"
#include "axivionplugin.h"
#include "axivionquery.h"
#include "axivionresultparser.h"
#include "axivionsettings.h"
#include "axiviontr.h"
#include <projectexplorer/project.h>
#include <utils/infolabel.h>
#include <utils/qtcassert.h>
#include <QPushButton>
#include <QTreeWidget>
#include <QVBoxLayout>
namespace Axivion::Internal {
const char PSK_PROJECTNAME[] = "Axivion.ProjectName";
AxivionProjectSettings::AxivionProjectSettings(ProjectExplorer::Project *project)
: m_project{project}
{
load();
connect(project, &ProjectExplorer::Project::settingsLoaded,
this, &AxivionProjectSettings::load);
connect(project, &ProjectExplorer::Project::aboutToSaveSettings,
this, &AxivionProjectSettings::save);
}
void AxivionProjectSettings::load()
{
m_dashboardProjectName = m_project->namedSettings(PSK_PROJECTNAME).toString();
}
void AxivionProjectSettings::save()
{
m_project->setNamedSettings(PSK_PROJECTNAME, m_dashboardProjectName);
}
AxivionProjectSettingsWidget::AxivionProjectSettingsWidget(ProjectExplorer::Project *project,
QWidget *parent)
: ProjectExplorer::ProjectSettingsWidget{parent}
, m_projectSettings(AxivionPlugin::projectSettings(project))
, m_globalSettings(AxivionPlugin::settings())
{
setUseGlobalSettingsCheckBoxVisible(false);
setUseGlobalSettingsLabelVisible(true);
setGlobalSettingsId("Axivion.Settings.General"); // FIXME move id to constants
// setup ui
auto verticalLayout = new QVBoxLayout(this);
verticalLayout->setContentsMargins(0, 0, 0, 0);
m_linkedProject = new QLabel(this);
verticalLayout->addWidget(m_linkedProject);
m_dashboardProjects = new QTreeWidget(this);
m_dashboardProjects->setHeaderHidden(true);
m_dashboardProjects->setRootIsDecorated(false);
verticalLayout->addWidget(new QLabel(Tr::tr("Dashboard projects:")));
verticalLayout->addWidget(m_dashboardProjects);
m_infoLabel = new Utils::InfoLabel(this);
m_infoLabel->setVisible(false);
verticalLayout->addWidget(m_infoLabel);
auto horizontalLayout = new QHBoxLayout;
horizontalLayout->setContentsMargins(0, 0, 0, 0);
m_fetchProjects = new QPushButton(Tr::tr("Fetch Projects"));
horizontalLayout->addWidget(m_fetchProjects);
m_link = new QPushButton(Tr::tr("Link Project"));
m_link->setEnabled(false);
horizontalLayout->addWidget(m_link);
m_unlink = new QPushButton(Tr::tr("Unlink Project"));
m_unlink->setEnabled(false);
horizontalLayout->addWidget(m_unlink);
verticalLayout->addLayout(horizontalLayout);
connect(m_dashboardProjects, &QTreeWidget::itemSelectionChanged,
this, &AxivionProjectSettingsWidget::updateEnabledStates);
connect(m_fetchProjects, &QPushButton::clicked,
this, &AxivionProjectSettingsWidget::fetchProjects);
connect(m_link, &QPushButton::clicked,
this, &AxivionProjectSettingsWidget::linkProject);
connect(m_unlink, &QPushButton::clicked,
this, &AxivionProjectSettingsWidget::unlinkProject);
connect(AxivionPlugin::instance(), &AxivionPlugin::settingsChanged,
this, &AxivionProjectSettingsWidget::onSettingsChanged);
updateUi();
}
void AxivionProjectSettingsWidget::fetchProjects()
{
m_dashboardProjects->clear();
m_fetchProjects->setEnabled(false);
m_infoLabel->setVisible(false);
// TODO perform query and populate m_dashboardProjects
const AxivionQuery query(AxivionQuery::DashboardInfo);
AxivionQueryRunner *runner = new AxivionQueryRunner(query, this);
connect(runner, &AxivionQueryRunner::resultRetrieved,
this, [this](const QByteArray &result){
onDashboardInfoReceived(ResultParser::parseDashboardInfo(result));
});
connect(runner, &AxivionQueryRunner::finished, this, [runner]{ runner->deleteLater(); });
runner->start();
}
void AxivionProjectSettingsWidget::onDashboardInfoReceived(const DashboardInfo &info)
{
if (!info.error.isEmpty()) {
m_infoLabel->setText(info.error);
m_infoLabel->setType(Utils::InfoLabel::Error);
m_infoLabel->setVisible(true);
updateEnabledStates();
return;
}
for (const Project &project : info.projects)
new QTreeWidgetItem(m_dashboardProjects, {project.name});
updateEnabledStates();
}
void AxivionProjectSettingsWidget::onSettingsChanged()
{
m_dashboardProjects->clear();
m_infoLabel->setVisible(false);
updateUi();
}
void AxivionProjectSettingsWidget::linkProject()
{
const QList<QTreeWidgetItem *> selected = m_dashboardProjects->selectedItems();
QTC_ASSERT(selected.size() == 1, return);
const QString projectName = selected.first()->text(0);
m_projectSettings->setDashboardProjectName(projectName);
updateUi();
AxivionPlugin::fetchProjectInfo(projectName);
}
void AxivionProjectSettingsWidget::unlinkProject()
{
QTC_ASSERT(!m_projectSettings->dashboardProjectName().isEmpty(), return);
m_projectSettings->setDashboardProjectName({});
updateUi();
AxivionPlugin::fetchProjectInfo({});
}
void AxivionProjectSettingsWidget::updateUi()
{
const QString projectName = m_projectSettings->dashboardProjectName();
if (projectName.isEmpty())
m_linkedProject->setText(Tr::tr("This project is not linked to a dashboard project."));
else
m_linkedProject->setText(Tr::tr("This project is linked to \"%1\".").arg(projectName));
updateEnabledStates();
}
void AxivionProjectSettingsWidget::updateEnabledStates()
{
const bool hasDashboardSettings = m_globalSettings->curl.isExecutableFile()
&& !m_globalSettings->server.dashboard.isEmpty()
&& !m_globalSettings->server.token.isEmpty();
const bool linked = !m_projectSettings->dashboardProjectName().isEmpty();
const bool linkable = m_dashboardProjects->topLevelItemCount()
&& !m_dashboardProjects->selectedItems().isEmpty();
m_fetchProjects->setEnabled(hasDashboardSettings);
m_link->setEnabled(!linked && linkable);
m_unlink->setEnabled(linked);
if (!hasDashboardSettings) {
m_infoLabel->setText(Tr::tr("Incomplete or misconfigured settings."));
m_infoLabel->setType(Utils::InfoLabel::NotOk);
m_infoLabel->setVisible(true);
}
}
} // Axivion::Internal

View File

@@ -0,0 +1,67 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#pragma once
#include "axivionsettings.h"
#include <projectexplorer/projectsettingswidget.h>
#include <QObject>
QT_BEGIN_NAMESPACE
class QLabel;
class QPushButton;
class QTreeWidget;
QT_END_NAMESPACE
namespace ProjectExplorer { class Project; }
namespace Utils { class InfoLabel; }
namespace Axivion::Internal {
class DashboardInfo;
class AxivionProjectSettings : public QObject
{
public:
explicit AxivionProjectSettings(ProjectExplorer::Project *project);
void setDashboardProjectName(const QString &name) { m_dashboardProjectName = name; }
QString dashboardProjectName() const { return m_dashboardProjectName; }
private:
void load();
void save();
ProjectExplorer::Project *m_project = nullptr;
QString m_dashboardProjectName;
};
class AxivionProjectSettingsWidget : public ProjectExplorer::ProjectSettingsWidget
{
public:
explicit AxivionProjectSettingsWidget(ProjectExplorer::Project *project,
QWidget *parent = nullptr);
private:
void fetchProjects();
void onDashboardInfoReceived(const DashboardInfo &info);
void onSettingsChanged();
void linkProject();
void unlinkProject();
void updateUi();
void updateEnabledStates();
AxivionProjectSettings *m_projectSettings = nullptr;
AxivionSettings *m_globalSettings;
QLabel *m_linkedProject = nullptr;
QTreeWidget *m_dashboardProjects = nullptr;
QPushButton *m_fetchProjects = nullptr;
QPushButton *m_link = nullptr;
QPushButton *m_unlink = nullptr;
Utils::InfoLabel *m_infoLabel = nullptr;
};
} // Axivion::Internal

View File

@@ -0,0 +1,97 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#include "axivionquery.h"
#include "axivionplugin.h"
#include "axivionsettings.h"
#include <utils/processenums.h>
#include <utils/qtcassert.h>
#include <QUrl>
using namespace Utils;
namespace Axivion::Internal {
AxivionQuery::AxivionQuery(QueryType type, const QStringList &parameters)
: m_type(type)
, m_parameters(parameters)
{
}
QString AxivionQuery::toString() const
{
QString query = "/api"; // common for all except RuleInfo
switch (m_type) {
case NoQuery:
return {};
case DashboardInfo:
return query;
case ProjectInfo:
QTC_ASSERT(m_parameters.size() == 1, return {});
query += "/projects/" + QUrl::toPercentEncoding(m_parameters.first());
return query;
case IssuesForFileList:
QTC_ASSERT(m_parameters.size() == 3, return {});
// FIXME shall we validate the kind? (some kinds do not support path filter)
query += "/projects/" + QUrl::toPercentEncoding(m_parameters.first())
+ "/issues?kind=" + m_parameters.at(1) + "&filter_path="
+ QUrl::toPercentEncoding(m_parameters.at(2)) + "&format=csv";
return query;
case RuleInfo:
QTC_ASSERT(m_parameters.size() == 2, return {});
query = "/projects/" + QUrl::toPercentEncoding(m_parameters.first())
+ "/issues/" + m_parameters.at(1) + "/rule";
return query;
}
return {};
}
AxivionQueryRunner::AxivionQueryRunner(const AxivionQuery &query, QObject *parent)
: QObject(parent)
{
const AxivionSettings *settings = AxivionPlugin::settings();
const AxivionServer server = settings->server;
QStringList args = server.curlArguments();
args << "-i";
args << "--header" << "Authorization: AxToken " + server.token;
QString url = server.dashboard;
while (url.endsWith('/')) url.chop(1);
url += query.toString();
args << url;
m_process.setCommand({settings->curl, args});
connect(&m_process, &Process::done, this, [this]{
if (m_process.result() != ProcessResult::FinishedWithSuccess) {
const int exitCode = m_process.exitCode();
if (m_process.exitStatus() == QProcess::NormalExit
&& (exitCode == 35 || exitCode == 60)
&& AxivionPlugin::handleCertificateIssue()) {
// prepend -k for re-requesting same query
CommandLine cmdline = m_process.commandLine();
cmdline.prependArgs({"-k"});
m_process.close();
m_process.setCommand(cmdline);
start();
return;
}
emit resultRetrieved(m_process.readAllRawStandardError());
} else {
emit resultRetrieved(m_process.readAllRawStandardOutput());
}
emit finished();
});
}
void AxivionQueryRunner::start()
{
QTC_ASSERT(!m_process.isRunning(), return);
m_process.start();
}
} // Axivion::Internal

View File

@@ -0,0 +1,40 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#pragma once
#include <utils/process.h>
#include <QObject>
namespace Axivion::Internal {
class AxivionQuery
{
public:
enum QueryType {NoQuery, DashboardInfo, ProjectInfo, IssuesForFileList, RuleInfo};
explicit AxivionQuery(QueryType type, const QStringList &parameters = {});
QString toString() const;
private:
QueryType m_type = NoQuery;
QStringList m_parameters;
};
class AxivionQueryRunner : public QObject
{
Q_OBJECT
public:
explicit AxivionQueryRunner(const AxivionQuery &query, QObject *parent = nullptr);
void start();
signals:
void finished();
void resultRetrieved(const QByteArray &json);
private:
Utils::Process m_process;
};
} // Axivion::Internal

View File

@@ -0,0 +1,347 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#include "axivionresultparser.h"
#include <utils/qtcassert.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include <utility>
namespace Axivion::Internal {
static std::pair<QByteArray, QByteArray> splitHeaderAndBody(const QByteArray &input)
{
QByteArray header;
QByteArray json;
int emptyLine = input.indexOf("\r\n\r\n"); // we always get \r\n as line separator
if (emptyLine != -1) {
header = input.left(emptyLine);
json = input.mid(emptyLine + 4);
} else {
json = input;
}
return {header, json};
}
static int httpStatus(const QByteArray &header)
{
int firstHeaderEnd = header.indexOf("\r\n");
if (firstHeaderEnd == -1)
return 600; // unexpected header
const QString firstLine = QString::fromUtf8(header.first(firstHeaderEnd));
static const QRegularExpression regex(R"(^HTTP/\d\.\d (\d{3}) .*$)");
const QRegularExpressionMatch match = regex.match(firstLine);
return match.hasMatch() ? match.captured(1).toInt() : 601;
}
static BaseResult prehandleHeader(const QByteArray &header, const QByteArray &body)
{
BaseResult result;
if (header.isEmpty()) {
result.error = QString::fromUtf8(body); // we likely had a curl problem
return result;
}
int status = httpStatus(header);
if ((status > 399) || (status > 299 && body.isEmpty())) { // FIXME handle some explicitly?
const QString statusStr = QString::number(status);
if (body.isEmpty() || body.startsWith('<')) // likely an html response or redirect
result.error = QLatin1String("(%1)").arg(statusStr);
else
result.error = QLatin1String("%1 (%2)").arg(QString::fromUtf8(body)).arg(statusStr);
}
return result;
}
static std::pair<BaseResult, QJsonDocument> prehandleHeaderAndBody(const QByteArray &header,
const QByteArray &body)
{
BaseResult result = prehandleHeader(header, body);
if (!result.error.isEmpty())
return {result, {}};
QJsonParseError error;
const QJsonDocument doc = QJsonDocument::fromJson(body, &error);
if (error.error != QJsonParseError::NoError) {
result.error = error.errorString();
return {result, doc};
}
if (!doc.isObject()) {
result.error = "Not an object.";
return {result, {}};
}
return {result, doc};
}
static User::UserType userTypeForString(const QString &type)
{
if (type == "DASHBOARD_USER")
return User::Dashboard;
if (type == "VIRTUAL_USER")
return User::Virtual;
return User::Unknown;
}
static User userFromJson(const QJsonObject &object)
{
User result;
if (object.isEmpty()) {
result.error = "Not a user object.";
return result;
}
result.name = object.value("name").toString();
result.displayName = object.value("displayName").toString();
result.type = userTypeForString(object.value("type").toString());
return result;
}
static QList<User> usersFromJson(const QJsonArray &array)
{
QList<User> result;
for (const QJsonValue &value : array) {
User user = userFromJson(value.toObject());
if (!user.error.isEmpty()) // add this error to result.error?
continue;
result.append(user);
}
return result;
}
static IssueCount issueCountFromJson(const QJsonObject &object)
{
IssueCount result;
if (object.isEmpty()) {
result.error = "Not an issue count object.";
return result;
}
result.added = object.value("Added").toInt();
result.removed = object.value("Removed").toInt();
result.total = object.value("Total").toInt();
return result;
}
static QList<IssueCount> issueCountsFromJson(const QJsonObject &object)
{
QList<IssueCount> result;
const QStringList keys = object.keys();
for (const QString &k : keys) {
IssueCount issue = issueCountFromJson(object.value(k).toObject());
if (!issue.error.isEmpty()) // add this error to result.error?
continue;
issue.issueKind = k;
result.append(issue);
}
return result;
}
static ResultVersion versionFromJson(const QJsonObject &object)
{
ResultVersion result;
if (object.isEmpty()) {
result.error = "Not a version object.";
return result;
}
const QJsonValue issuesValue = object.value("issueCounts");
if (!issuesValue.isObject()) {
result.error = "Not an object (issueCounts).";
return result;
}
result.issueCounts = issueCountsFromJson(issuesValue.toObject());
result.timeStamp = object.value("date").toString();
result.name = object.value("name").toString();
result.linesOfCode = object.value("linesOfCode").toInt();
return result;
}
static QList<ResultVersion> versionsFromJson(const QJsonArray &array)
{
QList<ResultVersion> result;
for (const QJsonValue &value : array) {
ResultVersion version = versionFromJson(value.toObject());
if (!version.error.isEmpty()) // add this error to result.error?
continue;
result.append(version);
}
return result;
}
static IssueKind issueKindFromJson(const QJsonObject &object)
{
IssueKind result;
if (object.isEmpty()) {
result.error = "Not an issue kind object.";
return result;
}
result.prefix = object.value("prefix").toString();
result.niceSingular = object.value("niceSingularName").toString();
result.nicePlural = object.value("nicePluralName").toString();
return result;
}
static QList<IssueKind> issueKindsFromJson(const QJsonArray &array)
{
QList<IssueKind> result;
for (const QJsonValue &value : array) {
IssueKind kind = issueKindFromJson(value.toObject());
if (!kind.error.isEmpty()) // add this error to result.error?
continue;
result.append(kind);
}
return result;
}
namespace ResultParser {
DashboardInfo parseDashboardInfo(const QByteArray &input)
{
DashboardInfo result;
auto [header, body] = splitHeaderAndBody(input);
auto [error, doc] = prehandleHeaderAndBody(header, body);
if (!error.error.isEmpty()) {
result.error = error.error;
return result;
}
const QJsonObject object = doc.object();
result.mainUrl = object.value("mainUrl").toString();
if (!object.contains("projects")) {
result.error = "Missing projects information.";
return result;
}
const QJsonValue projects = object.value("projects");
if (!projects.isArray()) {
result.error = "Projects information not an array.";
return result;
}
const QJsonArray array = projects.toArray();
for (const QJsonValue &val : array) {
if (!val.isObject())
continue;
const QJsonObject projectObject = val.toObject();
Project project;
project.name = projectObject.value("name").toString();
project.url = projectObject.value("url").toString();
if (project.name.isEmpty() || project.url.isEmpty())
continue;
result.projects.append(project);
}
return result;
}
ProjectInfo parseProjectInfo(const QByteArray &input)
{
ProjectInfo result;
auto [header, body] = splitHeaderAndBody(input);
auto [error, doc] = prehandleHeaderAndBody(header, body);
if (!error.error.isEmpty()) {
result.error = error.error;
return result;
}
const QJsonObject object = doc.object();
result.name = object.value("name").toString();
const QJsonValue usersValue = object.value("users");
if (!usersValue.isArray()) {
result.error = "Malformed json response (users).";
return result;
}
result.users = usersFromJson(usersValue.toArray());
const QJsonValue versionsValue = object.value("versions");
if (!versionsValue.isArray()) {
result.error = "Malformed json response (versions).";
return result;
}
result.versions = versionsFromJson(versionsValue.toArray());
const QJsonValue issueKindsValue = object.value("issueKinds");
if (!issueKindsValue.isArray()) {
result.error = "Malformed json response (issueKinds).";
return result;
}
result.issueKinds = issueKindsFromJson(issueKindsValue.toArray());
return result;
}
static QRegularExpression issueCsvLineRegex(const QByteArray &firstCsvLine)
{
QString pattern = "^";
for (const QByteArray &part : firstCsvLine.split(',')) {
const QString cleaned = QString::fromUtf8(part).remove(' ').chopped(1).mid(1);
pattern.append(QString("\"(?<" + cleaned + ">.*)\","));
}
pattern.chop(1); // remove last comma
pattern.append('$');
const QRegularExpression regex(pattern);
QTC_ASSERT(regex.isValid(), return {});
return regex;
}
static void parseCsvIssue(const QByteArray &csv, QList<ShortIssue> *issues)
{
QTC_ASSERT(issues, return);
bool first = true;
std::optional<QRegularExpression> regex;
for (auto &line : csv.split('\n')) {
if (first) {
regex.emplace(issueCsvLineRegex(line));
first = false;
if (regex.value().pattern().isEmpty())
return;
continue;
}
if (line.isEmpty())
continue;
const QRegularExpressionMatch match = regex->match(QString::fromUtf8(line));
QTC_ASSERT(match.hasMatch(), continue);
// FIXME: some of these are not present for all issue kinds! Limited to SV for now
ShortIssue issue;
issue.id = match.captured("Id");
issue.state = match.captured("State");
issue.errorNumber = match.captured("ErrorNumber");
issue.message = match.captured("Message");
issue.entity = match.captured("Entity");
issue.filePath = match.captured("Path");
issue.severity = match.captured("Severity");
issue.lineNumber = match.captured("Line").toInt();
issues->append(issue);
}
}
IssuesList parseIssuesList(const QByteArray &input)
{
IssuesList result;
auto [header, body] = splitHeaderAndBody(input);
BaseResult headerResult = prehandleHeader(header, body);
if (!headerResult.error.isEmpty()) {
result.error = headerResult.error;
return result;
}
parseCsvIssue(body, &result.issues);
return result;
}
QString parseRuleInfo(const QByteArray &input) // html result!
{
auto [header, body] = splitHeaderAndBody(input);
BaseResult headerResult = prehandleHeader(header, body);
if (!headerResult.error.isEmpty())
return QString();
return QString::fromLocal8Bit(body);
}
} // ResultParser
} // Axivion::Internal

View File

@@ -0,0 +1,101 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#pragma once
#include <QList>
namespace Axivion::Internal {
class BaseResult
{
public:
QString error;
};
class Project : public BaseResult
{
public:
QString name;
QString url;
};
class DashboardInfo : public BaseResult
{
public:
QString mainUrl;
QList<Project> projects;
};
class User : public BaseResult
{
public:
QString name;
QString displayName;
enum UserType { Dashboard, Virtual, Unknown } type;
};
class IssueKind : public BaseResult
{
public:
QString prefix;
QString niceSingular;
QString nicePlural;
};
class IssueCount : public BaseResult
{
public:
QString issueKind;
int total = 0;
int added = 0;
int removed = 0;
};
class ResultVersion : public BaseResult
{
public:
QString name;
QString timeStamp;
QList<IssueCount> issueCounts;
int linesOfCode = 0;
};
class ProjectInfo : public BaseResult
{
public:
QString name;
QList<User> users;
QList<ResultVersion> versions;
QList<IssueKind> issueKinds;
};
class ShortIssue : public BaseResult
{
public:
QString id;
QString state;
QString errorNumber;
QString message;
QString entity;
QString filePath;
QString severity;
int lineNumber = 0;
};
class IssuesList : public BaseResult
{
public:
QList<ShortIssue> issues;
};
namespace ResultParser {
DashboardInfo parseDashboardInfo(const QByteArray &input);
ProjectInfo parseProjectInfo(const QByteArray &input);
IssuesList parseIssuesList(const QByteArray &input);
QString parseRuleInfo(const QByteArray &input);
} // ResultParser
} // Axivion::Internal

View File

@@ -0,0 +1,131 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#include "axivionsettings.h"
#include <utils/hostosinfo.h>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSettings>
#include <QStandardPaths>
namespace Axivion::Internal {
const char curlKeyC[] = "Curl";
AxivionServer::AxivionServer(const Utils::Id &id, const QString &dashboard,
const QString &description, const QString &token)
: id(id)
, dashboard(dashboard)
, description(description)
, token(token)
{
}
bool AxivionServer::operator==(const AxivionServer &other) const
{
return id == other.id && dashboard == other.dashboard
&& description == other.description && token == other.token;
}
bool AxivionServer::operator!=(const AxivionServer &other) const
{
return !(*this == other);
}
QJsonObject AxivionServer::toJson() const
{
QJsonObject result;
result.insert("id", id.toString());
result.insert("dashboard", dashboard);
result.insert("description", description);
result.insert("token", token);
return result;
}
AxivionServer AxivionServer::fromJson(const QJsonObject &json)
{
const AxivionServer invalidServer;
const QJsonValue id = json.value("id");
if (id == QJsonValue::Undefined)
return invalidServer;
const QJsonValue dashboard = json.value("dashboard");
if (dashboard == QJsonValue::Undefined)
return invalidServer;
const QJsonValue description = json.value("description");
if (description == QJsonValue::Undefined)
return invalidServer;
const QJsonValue token = json.value("token");
if (token == QJsonValue::Undefined)
return invalidServer;
return { Utils::Id::fromString(id.toString()), dashboard.toString(),
description.toString(), token.toString() };
}
QStringList AxivionServer::curlArguments() const
{
QStringList args { "-sS" }; // silent, but show error
if (dashboard.startsWith("https://") && !validateCert)
args << "-k";
return args;
}
AxivionSettings::AxivionSettings()
{
}
static Utils::FilePath tokensFilePath(const QSettings *s)
{
return Utils::FilePath::fromString(s->fileName()).parentDir()
.pathAppended("qtcreator/axivion.json");
}
static void writeTokenFile(const Utils::FilePath &filePath, const AxivionServer &server)
{
QJsonDocument doc;
doc.setObject(server.toJson());
// FIXME error handling?
filePath.writeFileContents(doc.toJson());
filePath.setPermissions(QFile::ReadUser | QFile::WriteUser);
}
static AxivionServer readTokenFile(const Utils::FilePath &filePath)
{
if (!filePath.exists())
return {};
Utils::expected_str<QByteArray> contents = filePath.fileContents();
if (!contents)
return {};
const QJsonDocument doc = QJsonDocument::fromJson(*contents);
if (!doc.isObject())
return {};
return AxivionServer::fromJson(doc.object());
}
void AxivionSettings::toSettings(QSettings *s) const
{
writeTokenFile(tokensFilePath(s), server);
s->beginGroup("Axivion");
s->setValue(curlKeyC, curl.toVariant());
s->endGroup();
}
void AxivionSettings::fromSettings(QSettings *s)
{
s->beginGroup("Axivion");
curl = Utils::FilePath::fromVariant(curlKeyC);
s->endGroup();
server = readTokenFile(tokensFilePath(s));
if (curl.isEmpty() || !curl.exists()) {
const QString curlPath = QStandardPaths::findExecutable(
Utils::HostOsInfo::withExecutableSuffix("curl"));
if (!curlPath.isEmpty())
curl = Utils::FilePath::fromString(curlPath);
}
}
} // Axivion::Internal

View File

@@ -0,0 +1,51 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#pragma once
#include <utils/filepath.h>
#include <utils/id.h>
#include <QtGlobal>
QT_BEGIN_NAMESPACE
class QJsonObject;
class QSettings;
QT_END_NAMESPACE
namespace Axivion::Internal {
class AxivionServer
{
public:
AxivionServer() = default;
AxivionServer(const Utils::Id &id, const QString &dashboardUrl,
const QString &description, const QString &token);
bool operator==(const AxivionServer &other) const;
bool operator!=(const AxivionServer &other) const;
QJsonObject toJson() const;
static AxivionServer fromJson(const QJsonObject &json);
QStringList curlArguments() const;
Utils::Id id;
QString dashboard;
QString description;
QString token;
bool validateCert = true;
};
class AxivionSettings
{
public:
AxivionSettings();
void toSettings(QSettings *s) const;
void fromSettings(QSettings *s);
AxivionServer server; // shall we have more than one?
Utils::FilePath curl;
};
} // Axivion::Internal

View File

@@ -0,0 +1,208 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#include "axivionsettingspage.h"
#include "axivionplugin.h"
#include "axivionsettings.h"
#include "axiviontr.h"
#include <coreplugin/icore.h>
#include <utils/layoutbuilder.h>
#include <utils/pathchooser.h>
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
#include <QPushButton>
#include <QRegularExpression>
#include <QUuid>
#include <QVBoxLayout>
using namespace Utils;
namespace Axivion::Internal {
// may allow some invalid, but does some minimal check for legality
static bool hostValid(const QString &host)
{
static const QRegularExpression ip(R"(^(\d+).(\d+).(\d+).(\d+)$)");
static const QRegularExpression dn(R"(^([a-zA-Z0-9][a-zA-Z0-9-]+\.)+[a-zA-Z0-9][a-zA-Z0-9-]+$)");
const QRegularExpressionMatch match = ip.match(host);
if (match.hasMatch()) {
for (int i = 1; i < 5; ++i) {
int val = match.captured(i).toInt();
if (val < 0 || val > 255)
return false;
}
return true;
}
return (host == "localhost") || dn.match(host).hasMatch();
}
static bool isUrlValid(const QString &in)
{
const QUrl url(in);
return hostValid(url.host()) && (url.scheme() == "https" || url.scheme() == "http");
}
DashboardSettingsWidget::DashboardSettingsWidget(Mode mode, QWidget *parent)
: QWidget(parent)
, m_mode(mode)
{
auto labelStyle = mode == Display ? StringAspect::LabelDisplay : StringAspect::LineEditDisplay;
m_dashboardUrl.setLabelText(Tr::tr("Dashboard URL:"));
m_dashboardUrl.setDisplayStyle(labelStyle);
m_dashboardUrl.setValidationFunction([](FancyLineEdit *edit, QString *){
return isUrlValid(edit->text());
});
m_description.setLabelText(Tr::tr("Description:"));
m_description.setDisplayStyle(labelStyle);
m_description.setPlaceHolderText(Tr::tr("Non-empty description"));
m_token.setLabelText(Tr::tr("Access token:"));
m_token.setDisplayStyle(labelStyle);
m_token.setPlaceHolderText(Tr::tr("IDE Access Token"));
m_token.setVisible(mode == Edit);
using namespace Layouting;
Row {
Form {
m_dashboardUrl,
m_description,
m_token,
mode == Edit ? normalMargin : noMargin
}
}.attachTo(this);
auto checkValidity = [this] {
bool old = m_valid;
m_valid = isValid();
if (old != m_valid)
emit validChanged(m_valid);
};
if (mode == Edit) {
connect(&m_dashboardUrl, &StringAspect::valueChanged,
this, checkValidity);
connect(&m_description, &StringAspect::valueChanged,
this, checkValidity);
connect(&m_token, &StringAspect::valueChanged,
this, checkValidity);
}
}
AxivionServer DashboardSettingsWidget::dashboardServer() const
{
AxivionServer result;
if (m_id.isValid())
result.id = m_id;
else
result.id = m_mode == Edit ? Utils::Id::fromName(QUuid::createUuid().toByteArray()) : m_id;
result.dashboard = m_dashboardUrl.value();
result.description = m_description.value();
result.token = m_token.value();
return result;
}
void DashboardSettingsWidget::setDashboardServer(const AxivionServer &server)
{
m_id = server.id;
m_dashboardUrl.setValue(server.dashboard);
m_description.setValue(server.description);
m_token.setValue(server.token);
}
bool DashboardSettingsWidget::isValid() const
{
return !m_token.value().isEmpty() && !m_description.value().isEmpty()
&& isUrlValid(m_dashboardUrl.value());
}
class AxivionSettingsWidget : public Core::IOptionsPageWidget
{
public:
explicit AxivionSettingsWidget(AxivionSettings *settings);
void apply() override;
private:
void showEditServerDialog();
AxivionSettings *m_settings;
Utils::StringAspect m_curlPC;
DashboardSettingsWidget *m_dashboardDisplay = nullptr;
QPushButton *m_edit = nullptr;
};
AxivionSettingsWidget::AxivionSettingsWidget(AxivionSettings *settings)
: m_settings(settings)
{
using namespace Layouting;
m_dashboardDisplay = new DashboardSettingsWidget(DashboardSettingsWidget::Display, this);
m_dashboardDisplay->setDashboardServer(m_settings->server);
m_edit = new QPushButton(Tr::tr("Edit..."), this);
m_curlPC.setLabelText(Tr::tr("curl:"));
m_curlPC.setDisplayStyle(StringAspect::PathChooserDisplay);
m_curlPC.setExpectedKind(PathChooser::ExistingCommand);
m_curlPC.setFilePath(m_settings->curl);
Grid {
Form {
m_dashboardDisplay, br,
m_curlPC, br,
}, Column { m_edit, st }
}.attachTo(this);
connect(m_edit, &QPushButton::clicked, this, &AxivionSettingsWidget::showEditServerDialog);
}
void AxivionSettingsWidget::apply()
{
m_settings->server = m_dashboardDisplay->dashboardServer();
m_settings->curl = m_curlPC.filePath();
m_settings->toSettings(Core::ICore::settings());
emit AxivionPlugin::instance()->settingsChanged();
}
void AxivionSettingsWidget::showEditServerDialog()
{
const AxivionServer old = m_dashboardDisplay->dashboardServer();
QDialog d;
d.setWindowTitle(Tr::tr("Edit Dashboard Configuration"));
QVBoxLayout *layout = new QVBoxLayout;
DashboardSettingsWidget *dashboardWidget = new DashboardSettingsWidget(DashboardSettingsWidget::Edit, this);
dashboardWidget->setDashboardServer(old);
layout->addWidget(dashboardWidget);
auto buttons = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok, this);
auto ok = buttons->button(QDialogButtonBox::Ok);
ok->setEnabled(m_dashboardDisplay->isValid());
connect(buttons->button(QDialogButtonBox::Cancel), &QPushButton::clicked, &d, &QDialog::reject);
connect(ok, &QPushButton::clicked, &d, &QDialog::accept);
connect(dashboardWidget, &DashboardSettingsWidget::validChanged,
ok, &QPushButton::setEnabled);
layout->addWidget(buttons);
d.setLayout(layout);
d.resize(500, 200);
if (d.exec() != QDialog::Accepted)
return;
if (dashboardWidget->isValid()) {
const AxivionServer server = dashboardWidget->dashboardServer();
if (server != old)
m_dashboardDisplay->setDashboardServer(server);
}
}
AxivionSettingsPage::AxivionSettingsPage(AxivionSettings *settings)
: m_settings(settings)
{
setId("Axivion.Settings.General");
setDisplayName(Tr::tr("General"));
setCategory("XY.Axivion");
setDisplayCategory(Tr::tr("Axivion"));
setCategoryIconPath(":/axivion/images/axivion.png");
setWidgetCreator([this] { return new AxivionSettingsWidget(m_settings); });
}
} // Axivion::Internal

View File

@@ -0,0 +1,52 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#pragma once
#include <coreplugin/dialogs/ioptionspage.h>
#include <utils/aspects.h>
#include <utils/id.h>
#include <QPointer>
#include <QWidget>
namespace Axivion::Internal {
class AxivionServer;
class AxivionSettings;
class AxivionSettingsWidget;
class DashboardSettingsWidget : public QWidget
{
Q_OBJECT
public:
enum Mode { Display, Edit };
explicit DashboardSettingsWidget(Mode m = Display, QWidget *parent = nullptr);
AxivionServer dashboardServer() const;
void setDashboardServer(const AxivionServer &server);
bool isValid() const;
signals:
void validChanged(bool valid);
private:
Mode m_mode = Display;
Utils::Id m_id;
Utils::StringAspect m_dashboardUrl;
Utils::StringAspect m_description;
Utils::StringAspect m_token;
bool m_valid = false;
};
class AxivionSettingsPage : public Core::IOptionsPage
{
public:
explicit AxivionSettingsPage(AxivionSettings *settings);
private:
AxivionSettings *m_settings;
};
} // Axivion::Internal

View File

@@ -0,0 +1,15 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#pragma once
#include <QCoreApplication>
namespace Axivion {
struct Tr
{
Q_DECLARE_TR_FUNCTIONS(Axivion)
};
} // Axivion

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B