Axivion: Support named filters

Support loading, displaying and applying of existing
named filters to the current displayed issues.

Change-Id: Iab079228c8ffbfa75d1bc4ff3d378fc9b6bd1c53
Reviewed-by: Jarek Kobus <jaroslaw.kobus@qt.io>
This commit is contained in:
Christian Stenger
2024-12-04 13:47:11 +01:00
parent 94121b4922
commit daf0137c89
6 changed files with 260 additions and 6 deletions

View File

@@ -185,6 +185,7 @@ public:
void updateUi(const QString &kind); void updateUi(const QString &kind);
void initDashboardList(const QString &preferredProject = {}); void initDashboardList(const QString &preferredProject = {});
void resetDashboard(); void resetDashboard();
void updateNamedFilters();
const std::optional<Dto::TableInfoDto> currentTableInfo() const { return m_currentTableInfo; } const std::optional<Dto::TableInfoDto> currentTableInfo() const { return m_currentTableInfo; }
IssueListSearch searchFromUi() const; IssueListSearch searchFromUi() const;
@@ -201,6 +202,7 @@ private:
void onSearchParameterChanged(); void onSearchParameterChanged();
void updateVersionItemsEnabledState(); void updateVersionItemsEnabledState();
void updateBasicProjectInfo(const std::optional<Dto::ProjectInfoDto> &info); void updateBasicProjectInfo(const std::optional<Dto::ProjectInfoDto> &info);
void updateAllFilters(const QVariant &namedFilter);
void setFiltersEnabled(bool enabled); void setFiltersEnabled(bool enabled);
void fetchTable(); void fetchTable();
void fetchIssues(const IssueListSearch &search); void fetchIssues(const IssueListSearch &search);
@@ -219,6 +221,7 @@ private:
QComboBox *m_ownerFilter = nullptr; QComboBox *m_ownerFilter = nullptr;
QComboBox *m_versionStart = nullptr; QComboBox *m_versionStart = nullptr;
QComboBox *m_versionEnd = nullptr; QComboBox *m_versionEnd = nullptr;
QComboBox *m_namedFilters = nullptr;
Guard m_signalBlocker; Guard m_signalBlocker;
QLineEdit *m_pathGlobFilter = nullptr; // FancyLineEdit instead? QLineEdit *m_pathGlobFilter = nullptr; // FancyLineEdit instead?
QLabel *m_totalRows = nullptr; QLabel *m_totalRows = nullptr;
@@ -334,6 +337,15 @@ IssuesWidget::IssuesWidget(QWidget *parent)
m_pathGlobFilter->setPlaceholderText(Tr::tr("Path globbing")); m_pathGlobFilter->setPlaceholderText(Tr::tr("Path globbing"));
connect(m_pathGlobFilter, &QLineEdit::textEdited, this, &IssuesWidget::onSearchParameterChanged); connect(m_pathGlobFilter, &QLineEdit::textEdited, this, &IssuesWidget::onSearchParameterChanged);
m_namedFilters = new QComboBox(this);
m_namedFilters->setToolTip(Tr::tr("Named filters"));
m_namedFilters->setMinimumContentsLength(25);
connect(m_namedFilters, &QComboBox::currentIndexChanged, this, [this] {
if (m_signalBlocker.isLocked())
return;
updateAllFilters(m_namedFilters->currentData());
});
m_issuesView = new BaseTreeView(this); m_issuesView = new BaseTreeView(this);
m_issuesView->setFrameShape(QFrame::StyledPanel); // Bring back Qt default m_issuesView->setFrameShape(QFrame::StyledPanel); // Bring back Qt default
m_issuesView->setFrameShadow(QFrame::Sunken); // Bring back Qt default m_issuesView->setFrameShadow(QFrame::Sunken); // Bring back Qt default
@@ -372,7 +384,7 @@ IssuesWidget::IssuesWidget(QWidget *parent)
Column { Column {
Row { m_dashboards, m_dashboardProjects, empty, m_typesLayout, st, m_versionStart, m_versionEnd, st }, Row { m_dashboards, m_dashboardProjects, empty, m_typesLayout, st, m_versionStart, m_versionEnd, st },
Row { m_addedFilter, m_removedFilter, Space(1), m_ownerFilter, m_pathGlobFilter }, Row { m_addedFilter, m_removedFilter, Space(1), m_ownerFilter, m_pathGlobFilter, m_namedFilters },
m_stack, m_stack,
Row { st, m_totalRows } Row { st, m_totalRows }
}.attachTo(widget); }.attachTo(widget);
@@ -436,6 +448,28 @@ void IssuesWidget::resetDashboard()
m_dashboardListUninitialized = true; m_dashboardListUninitialized = true;
} }
void IssuesWidget::updateNamedFilters()
{
QList<NamedFilter> globalFilters;
QList<NamedFilter> userFilters;
knownNamedFilters(&globalFilters, &userFilters);
Utils::sort(globalFilters, [](const NamedFilter &lhs, const NamedFilter &rhs) {
return lhs.displayName < rhs.displayName;
});
Utils::sort(userFilters, [](const NamedFilter &lhs, const NamedFilter &rhs) {
return lhs.displayName < rhs.displayName;
});
GuardLocker lock(m_signalBlocker);
m_namedFilters->clear();
m_namedFilters->addItem(Tr::tr("Show all")); // no active named filter
for (const auto &it : userFilters)
m_namedFilters->addItem(it.displayName, QVariant::fromValue(it));
for (const auto &it : globalFilters)
m_namedFilters->addItem(it.displayName, QVariant::fromValue(it));
}
void IssuesWidget::initDashboardList(const QString &preferredProject) void IssuesWidget::initDashboardList(const QString &preferredProject)
{ {
const QString currentProject = preferredProject.isEmpty() ? m_dashboardProjects->currentText() const QString currentProject = preferredProject.isEmpty() ? m_dashboardProjects->currentText()
@@ -477,10 +511,13 @@ void IssuesWidget::reinitProjectList(const QString &currentProject)
m_issuesView->hideProgressIndicator(); m_issuesView->hideProgressIndicator();
return; return;
} }
GuardLocker lock(m_signalBlocker); {
m_dashboardProjects->addItems(info->projects); GuardLocker lock(m_signalBlocker);
if (!currentProject.isEmpty() && info->projects.contains(currentProject)) m_dashboardProjects->addItems(info->projects);
m_dashboardProjects->setCurrentText(currentProject); if (!currentProject.isEmpty() && info->projects.contains(currentProject))
m_dashboardProjects->setCurrentText(currentProject);
}
fetchNamedFilters();
}; };
{ {
GuardLocker lock(m_signalBlocker); GuardLocker lock(m_signalBlocker);
@@ -693,6 +730,7 @@ void IssuesWidget::updateBasicProjectInfo(const std::optional<Dto::ProjectInfoDt
m_versionStart->clear(); m_versionStart->clear();
m_versionEnd->clear(); m_versionEnd->clear();
m_pathGlobFilter->clear(); m_pathGlobFilter->clear();
m_namedFilters->clear();
m_currentProject.clear(); m_currentProject.clear();
m_currentPrefix.clear(); m_currentPrefix.clear();
@@ -755,6 +793,30 @@ void IssuesWidget::updateBasicProjectInfo(const std::optional<Dto::ProjectInfoDt
updateVersionItemsEnabledState(); updateVersionItemsEnabledState();
} }
void IssuesWidget::updateAllFilters(const QVariant &namedFilter)
{
NamedFilter nf;
if (namedFilter.isValid())
nf = namedFilter.value<NamedFilter>();
const bool clearOnly = nf.key.isEmpty();
const std::optional<Dto::NamedFilterInfoDto> filterInfo
= clearOnly ? std::nullopt : namedFilterInfoForKey(nf.key, nf.global);
GuardLocker lock(m_signalBlocker);
if (filterInfo) {
m_headerView->updateExistingColumnInfos(filterInfo->filters, filterInfo->sorters);
const auto it = filterInfo->filters.find("any path");
if (it != filterInfo->filters.cend())
m_pathGlobFilter->setText(it->second);
else
m_pathGlobFilter->clear();
} else {
// clear all filters / sorters
m_headerView->updateExistingColumnInfos({}, std::nullopt);
m_pathGlobFilter->clear();
}
}
void IssuesWidget::setFiltersEnabled(bool enabled) void IssuesWidget::setFiltersEnabled(bool enabled)
{ {
m_addedFilter->setEnabled(enabled); m_addedFilter->setEnabled(enabled);
@@ -763,6 +825,7 @@ void IssuesWidget::setFiltersEnabled(bool enabled)
m_versionStart->setEnabled(enabled); m_versionStart->setEnabled(enabled);
m_versionEnd->setEnabled(enabled); m_versionEnd->setEnabled(enabled);
m_pathGlobFilter->setEnabled(enabled); m_pathGlobFilter->setEnabled(enabled);
m_namedFilters->setEnabled(enabled);
} }
IssueListSearch IssuesWidget::searchFromUi() const IssueListSearch IssuesWidget::searchFromUi() const
@@ -966,6 +1029,7 @@ public:
void setIssueDetailsHtml(const QString &html) { m_issueDetails->setHtml(html); } void setIssueDetailsHtml(const QString &html) { m_issueDetails->setHtml(html); }
void handleAnchorClicked(const QUrl &url); void handleAnchorClicked(const QUrl &url);
void updateToolbarButtons(); void updateToolbarButtons();
void updateNamedFilters();
private: private:
void openFilterHelp(); void openFilterHelp();
@@ -1144,6 +1208,11 @@ void AxivionPerspective::updateToolbarButtons()
m_showFilterHelp->setEnabled(pInfo && pInfo->issueFilterHelp); m_showFilterHelp->setEnabled(pInfo && pInfo->issueFilterHelp);
} }
void AxivionPerspective::updateNamedFilters()
{
m_issuesWidget->updateNamedFilters();
}
void AxivionPerspective::openFilterHelp() void AxivionPerspective::openFilterHelp()
{ {
const std::optional<Dto::ProjectInfoDto> projInfo = projectInfo(); const std::optional<Dto::ProjectInfoDto> projInfo = projectInfo();
@@ -1213,4 +1282,10 @@ void updatePerspectiveToolbar()
theAxivionPerspective->updateToolbarButtons(); theAxivionPerspective->updateToolbarButtons();
} }
void updateNamedFilters()
{
QTC_ASSERT(theAxivionPerspective, return);
theAxivionPerspective->updateNamedFilters();
}
} // Axivion::Internal } // Axivion::Internal

View File

@@ -15,5 +15,6 @@ void reinitDashboard(const QString &projectName);
void resetDashboard(); void resetDashboard();
void updateIssueDetails(const QString &html); void updateIssueDetails(const QString &html);
void updatePerspectiveToolbar(); void updatePerspectiveToolbar();
void updateNamedFilters();
} // Axivion::Internal } // Axivion::Internal

View File

@@ -155,7 +155,15 @@ static DashboardInfo toDashboardInfo(const GetDtoStorage<Dto::DashboardInfoDto>
projectUrls.insert(project.name, project.url); projectUrls.insert(project.name, project.url);
} }
} }
return {dashboardStorage.url, versionNumber, projects, projectUrls, infoDto.checkCredentialsUrl}; return {
dashboardStorage.url,
versionNumber,
projects,
projectUrls,
infoDto.checkCredentialsUrl,
infoDto.namedFiltersUrl,
infoDto.userNamedFiltersUrl
};
} }
QUrlQuery IssueListSearch::toUrlQuery(QueryMode mode) const QUrlQuery IssueListSearch::toUrlQuery(QueryMode mode) const
@@ -213,6 +221,7 @@ public:
void handleIssuesForFile(const Dto::FileViewDto &fileView); void handleIssuesForFile(const Dto::FileViewDto &fileView);
void enableInlineIssues(bool enable); void enableInlineIssues(bool enable);
void fetchIssueInfo(const QString &id); void fetchIssueInfo(const QString &id);
void fetchNamedFilters();
void onSessionLoaded(const QString &sessionName); void onSessionLoaded(const QString &sessionName);
void onAboutToSaveSession(); void onAboutToSaveSession();
@@ -229,11 +238,14 @@ public:
std::optional<DashboardInfo> m_dashboardInfo; std::optional<DashboardInfo> m_dashboardInfo;
std::optional<Dto::ProjectInfoDto> m_currentProjectInfo; std::optional<Dto::ProjectInfoDto> m_currentProjectInfo;
std::optional<QString> m_analysisVersion; std::optional<QString> m_analysisVersion;
QList<Dto::NamedFilterInfoDto> m_globalNamedFilters;
QList<Dto::NamedFilterInfoDto> m_userNamedFilters;
Project *m_project = nullptr; Project *m_project = nullptr;
bool m_runningQuery = false; bool m_runningQuery = false;
TaskTreeRunner m_taskTreeRunner; TaskTreeRunner m_taskTreeRunner;
std::unordered_map<IDocument *, std::unique_ptr<TaskTree>> m_docMarksTrees; std::unordered_map<IDocument *, std::unique_ptr<TaskTree>> m_docMarksTrees;
TaskTreeRunner m_issueInfoRunner; TaskTreeRunner m_issueInfoRunner;
TaskTreeRunner m_namedFilterRunner;
FileInProjectFinder m_fileFinder; // FIXME maybe obsolete when path mapping is implemented FileInProjectFinder m_fileFinder; // FIXME maybe obsolete when path mapping is implemented
QMetaObject::Connection m_fileFinderConnection; QMetaObject::Connection m_fileFinderConnection;
QHash<FilePath, QSet<TextMark *>> m_allMarks; QHash<FilePath, QSet<TextMark *>> m_allMarks;
@@ -279,6 +291,46 @@ std::optional<Dto::ProjectInfoDto> projectInfo()
return dd->m_currentProjectInfo; return dd->m_currentProjectInfo;
} }
void fetchNamedFilters()
{
QTC_ASSERT(dd, return);
dd->fetchNamedFilters();
}
void knownNamedFilters(QList<NamedFilter> *global, QList<NamedFilter> *user)
{
QTC_ASSERT(dd, return);
QTC_ASSERT(global, return);
QTC_ASSERT(user, return);
*global = Utils::transform(dd->m_globalNamedFilters, [](const Dto::NamedFilterInfoDto &dto) {
return NamedFilter{dto.key, dto.displayName, true};
});
*user = Utils::transform(dd->m_userNamedFilters, [](const Dto::NamedFilterInfoDto &dto) {
return NamedFilter{dto.key, dto.displayName, false};
});
}
std::optional<Dto::NamedFilterInfoDto> namedFilterInfoForKey(const QString &key, bool global)
{
QTC_ASSERT(dd, return std::nullopt);
const auto findFilter = [](const QList<Dto::NamedFilterInfoDto> filters, const QString &key)
-> std::optional<Dto::NamedFilterInfoDto> {
const int index = Utils::indexOf(filters, [key](const Dto::NamedFilterInfoDto &dto) {
return dto.key == key;
});
if (index == -1)
return std::nullopt;
return filters.at(index);
};
if (global)
return findFilter(dd->m_globalNamedFilters, key);
else
return findFilter(dd->m_userNamedFilters, key);
}
// FIXME: extend to give some details? // FIXME: extend to give some details?
// FIXME: move when curl is no more in use? // FIXME: move when curl is no more in use?
bool handleCertificateIssue(const Utils::Id &serverId) bool handleCertificateIssue(const Utils::Id &serverId)
@@ -388,6 +440,7 @@ static QByteArray contentTypeData(ContentType contentType)
{ {
switch (contentType) { switch (contentType) {
case ContentType::Html: return s_htmlContentType; case ContentType::Html: return s_htmlContentType;
case ContentType::Json: return s_jsonContentType;
case ContentType::PlainText: return s_plaintextContentType; case ContentType::PlainText: return s_plaintextContentType;
case ContentType::Svg: return s_svgContentType; case ContentType::Svg: return s_svgContentType;
} }
@@ -910,6 +963,62 @@ void AxivionPluginPrivate::fetchIssueInfo(const QString &id)
}); });
} }
static QList<Dto::NamedFilterInfoDto> extractNamedFiltersFromJsonArray(const QByteArray &json)
{
QList<Dto::NamedFilterInfoDto> result;
QJsonParseError error;
const QJsonDocument doc = QJsonDocument::fromJson(json, &error);
if (error.error != QJsonParseError::NoError)
return result;
if (!doc.isArray())
return result;
const QJsonArray array = doc.array();
for (const QJsonValue &value : array) {
if (!value.isObject())
continue;
const QJsonDocument objDocument(value.toObject());
const auto filter = Dto::NamedFilterInfoDto::deserializeExpected(objDocument.toJson());
if (filter)
result.append(*filter);
}
return result;
}
void AxivionPluginPrivate::fetchNamedFilters()
{
QTC_ASSERT(m_dashboardInfo, return);
// use simple downloadDatarecipe() as we cannot handle an array of a dto at the moment
const Storage<DownloadData> globalStorage;
const Storage<DownloadData> userStorage;
const auto onSetup = [this, globalStorage, userStorage] {
QTC_ASSERT(m_dashboardInfo, return);
globalStorage->inputUrl = m_dashboardInfo->source.resolved(
*m_dashboardInfo->globalNamedFilters);
globalStorage->expectedContentType = ContentType::Json;
userStorage->inputUrl = m_dashboardInfo->source.resolved(
*m_dashboardInfo->userNamedFilters);
userStorage->expectedContentType = ContentType::Json;
};
const auto onDone = [this, globalStorage, userStorage] {
m_globalNamedFilters = extractNamedFiltersFromJsonArray(globalStorage->outputData);
m_userNamedFilters = extractNamedFiltersFromJsonArray(userStorage->outputData);
updateNamedFilters();
};
Group namedFiltersGroup = Group {
globalStorage,
userStorage,
onGroupSetup(onSetup),
downloadDataRecipe(globalStorage) || successItem,
downloadDataRecipe(userStorage) || successItem,
onGroupDone(onDone)
};
m_namedFilterRunner.start(namedFiltersGroup);
}
void AxivionPluginPrivate::handleOpenedDocs() void AxivionPluginPrivate::handleOpenedDocs()
{ {
const QList<IDocument *> openDocuments = DocumentModel::openedDocuments(); const QList<IDocument *> openDocuments = DocumentModel::openedDocuments();
@@ -1086,7 +1195,10 @@ void switchActiveDashboardId(const Id &toDashboardId)
dd->m_apiToken.reset(); dd->m_apiToken.reset();
dd->m_dashboardInfo.reset(); dd->m_dashboardInfo.reset();
dd->m_currentProjectInfo.reset(); dd->m_currentProjectInfo.reset();
dd->m_globalNamedFilters.clear();
dd->m_userNamedFilters.clear();
updatePerspectiveToolbar(); updatePerspectiveToolbar();
updateNamedFilters();
} }
const std::optional<DashboardInfo> currentDashboardInfo() const std::optional<DashboardInfo> currentDashboardInfo()

View File

@@ -62,10 +62,13 @@ public:
QStringList projects; QStringList projects;
QHash<QString, QUrl> projectUrls; QHash<QString, QUrl> projectUrls;
std::optional<QUrl> checkCredentialsUrl; std::optional<QUrl> checkCredentialsUrl;
std::optional<QUrl> globalNamedFilters;
std::optional<QUrl> userNamedFilters;
}; };
enum class ContentType { enum class ContentType {
Html, Html,
Json,
PlainText, PlainText,
Svg Svg
}; };
@@ -101,6 +104,18 @@ Tasking::Group lineMarkerRecipe(const Utils::FilePath &filePath, const LineMarke
void fetchDashboardAndProjectInfo(const DashboardInfoHandler &handler, const QString &projectName); void fetchDashboardAndProjectInfo(const DashboardInfoHandler &handler, const QString &projectName);
std::optional<Dto::ProjectInfoDto> projectInfo(); std::optional<Dto::ProjectInfoDto> projectInfo();
struct NamedFilter
{
QString key;
QString displayName;
bool global = false;
};
void fetchNamedFilters();
void knownNamedFilters(QList<NamedFilter> *global, QList<NamedFilter> *user);
std::optional<Dto::NamedFilterInfoDto> namedFilterInfoForKey(const QString &key, bool global);
bool handleCertificateIssue(); bool handleCertificateIssue();
QIcon iconForIssue(const std::optional<Dto::IssueKind> &issueKind); QIcon iconForIssue(const std::optional<Dto::IssueKind> &issueKind);
@@ -117,3 +132,4 @@ Utils::FilePath findFileForIssuePath(const Utils::FilePath &issuePath);
} // Axivion::Internal } // Axivion::Internal
Q_DECLARE_METATYPE(Axivion::Internal::NamedFilter)

View File

@@ -5,6 +5,7 @@
#include "axiviontr.h" #include "axiviontr.h"
#include <utils/algorithm.h>
#include <utils/fancylineedit.h> #include <utils/fancylineedit.h>
#include <utils/icon.h> #include <utils/icon.h>
#include <utils/layoutbuilder.h> #include <utils/layoutbuilder.h>
@@ -211,6 +212,51 @@ const QMap<QString, QString> IssueHeaderView::currentFilterMapping() const
return filter; return filter;
} }
void IssueHeaderView::updateExistingColumnInfos(
const std::map<QString, QString> &filters,
const std::optional<std::vector<Dto::SortInfoDto>> &sorters)
{
// update filters..
for (int i = 0, end = m_columnInfoList.size(); i < end; ++i) {
ColumnInfo &info = m_columnInfoList[i];
const auto filterItem = filters.find(info.key);
if (filterItem == filters.end())
info.filter.reset();
else
info.filter.emplace(filterItem->second);
if (sorters) { // ..and sorters if needed
bool found = false;
for (const Dto::SortInfoDto &dto : *sorters) {
if (dto.key != info.key)
continue;
info.sortOrder = dto.getDirectionEnum() == Dto::SortDirection::asc
? Qt::AscendingOrder : Qt::DescendingOrder;
found = true;
}
if (!found)
info.sortOrder.reset();
} else { // or clear them
info.sortOrder.reset();
}
}
// update sort order
m_currentSortIndexes.clear();
if (sorters) {
for (const Dto::SortInfoDto &dto : *sorters) {
int index = Utils::indexOf(m_columnInfoList, [key = dto.key](const ColumnInfo &ci) {
return ci.key == key;
});
if (index == -1) // legit
continue;
m_currentSortIndexes << index;
}
}
// inform UI
emit filterChanged();
}
void IssueHeaderView::mousePressEvent(QMouseEvent *event) void IssueHeaderView::mousePressEvent(QMouseEvent *event)
{ {
if (event->button() == Qt::LeftButton) { if (event->button() == Qt::LeftButton) {

View File

@@ -3,6 +3,8 @@
#pragma once #pragma once
#include "dashboard/dto.h"
#include <QHeaderView> #include <QHeaderView>
#include <QList> #include <QList>
@@ -30,6 +32,8 @@ public:
const QString currentSortString() const; const QString currentSortString() const;
const QMap<QString, QString> currentFilterMapping() const; const QMap<QString, QString> currentFilterMapping() const;
void updateExistingColumnInfos(const std::map<QString, QString> &filters,
const std::optional<std::vector<Dto::SortInfoDto>> &sorters);
signals: signals:
void filterChanged(); void filterChanged();
void sortTriggered(); void sortTriggered();