From daf0137c8987b84cf6b062ac29b3ee1f658ebfa4 Mon Sep 17 00:00:00 2001 From: Christian Stenger Date: Wed, 4 Dec 2024 13:47:11 +0100 Subject: [PATCH] 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 --- src/plugins/axivion/axivionperspective.cpp | 85 ++++++++++++++- src/plugins/axivion/axivionperspective.h | 1 + src/plugins/axivion/axivionplugin.cpp | 114 ++++++++++++++++++++- src/plugins/axivion/axivionplugin.h | 16 +++ src/plugins/axivion/issueheaderview.cpp | 46 +++++++++ src/plugins/axivion/issueheaderview.h | 4 + 6 files changed, 260 insertions(+), 6 deletions(-) diff --git a/src/plugins/axivion/axivionperspective.cpp b/src/plugins/axivion/axivionperspective.cpp index 41a59acdbf6..e7164dfd626 100644 --- a/src/plugins/axivion/axivionperspective.cpp +++ b/src/plugins/axivion/axivionperspective.cpp @@ -185,6 +185,7 @@ public: void updateUi(const QString &kind); void initDashboardList(const QString &preferredProject = {}); void resetDashboard(); + void updateNamedFilters(); const std::optional currentTableInfo() const { return m_currentTableInfo; } IssueListSearch searchFromUi() const; @@ -201,6 +202,7 @@ private: void onSearchParameterChanged(); void updateVersionItemsEnabledState(); void updateBasicProjectInfo(const std::optional &info); + void updateAllFilters(const QVariant &namedFilter); void setFiltersEnabled(bool enabled); void fetchTable(); void fetchIssues(const IssueListSearch &search); @@ -219,6 +221,7 @@ private: QComboBox *m_ownerFilter = nullptr; QComboBox *m_versionStart = nullptr; QComboBox *m_versionEnd = nullptr; + QComboBox *m_namedFilters = nullptr; Guard m_signalBlocker; QLineEdit *m_pathGlobFilter = nullptr; // FancyLineEdit instead? QLabel *m_totalRows = nullptr; @@ -334,6 +337,15 @@ IssuesWidget::IssuesWidget(QWidget *parent) m_pathGlobFilter->setPlaceholderText(Tr::tr("Path globbing")); 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->setFrameShape(QFrame::StyledPanel); // Bring back Qt default m_issuesView->setFrameShadow(QFrame::Sunken); // Bring back Qt default @@ -372,7 +384,7 @@ IssuesWidget::IssuesWidget(QWidget *parent) Column { 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, Row { st, m_totalRows } }.attachTo(widget); @@ -436,6 +448,28 @@ void IssuesWidget::resetDashboard() m_dashboardListUninitialized = true; } +void IssuesWidget::updateNamedFilters() +{ + QList globalFilters; + QList 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) { const QString currentProject = preferredProject.isEmpty() ? m_dashboardProjects->currentText() @@ -477,10 +511,13 @@ void IssuesWidget::reinitProjectList(const QString ¤tProject) m_issuesView->hideProgressIndicator(); return; } - GuardLocker lock(m_signalBlocker); - m_dashboardProjects->addItems(info->projects); - if (!currentProject.isEmpty() && info->projects.contains(currentProject)) - m_dashboardProjects->setCurrentText(currentProject); + { + GuardLocker lock(m_signalBlocker); + m_dashboardProjects->addItems(info->projects); + if (!currentProject.isEmpty() && info->projects.contains(currentProject)) + m_dashboardProjects->setCurrentText(currentProject); + } + fetchNamedFilters(); }; { GuardLocker lock(m_signalBlocker); @@ -693,6 +730,7 @@ void IssuesWidget::updateBasicProjectInfo(const std::optionalclear(); m_versionEnd->clear(); m_pathGlobFilter->clear(); + m_namedFilters->clear(); m_currentProject.clear(); m_currentPrefix.clear(); @@ -755,6 +793,30 @@ void IssuesWidget::updateBasicProjectInfo(const std::optional(); + const bool clearOnly = nf.key.isEmpty(); + const std::optional 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) { m_addedFilter->setEnabled(enabled); @@ -763,6 +825,7 @@ void IssuesWidget::setFiltersEnabled(bool enabled) m_versionStart->setEnabled(enabled); m_versionEnd->setEnabled(enabled); m_pathGlobFilter->setEnabled(enabled); + m_namedFilters->setEnabled(enabled); } IssueListSearch IssuesWidget::searchFromUi() const @@ -966,6 +1029,7 @@ public: void setIssueDetailsHtml(const QString &html) { m_issueDetails->setHtml(html); } void handleAnchorClicked(const QUrl &url); void updateToolbarButtons(); + void updateNamedFilters(); private: void openFilterHelp(); @@ -1144,6 +1208,11 @@ void AxivionPerspective::updateToolbarButtons() m_showFilterHelp->setEnabled(pInfo && pInfo->issueFilterHelp); } +void AxivionPerspective::updateNamedFilters() +{ + m_issuesWidget->updateNamedFilters(); +} + void AxivionPerspective::openFilterHelp() { const std::optional projInfo = projectInfo(); @@ -1213,4 +1282,10 @@ void updatePerspectiveToolbar() theAxivionPerspective->updateToolbarButtons(); } +void updateNamedFilters() +{ + QTC_ASSERT(theAxivionPerspective, return); + theAxivionPerspective->updateNamedFilters(); +} + } // Axivion::Internal diff --git a/src/plugins/axivion/axivionperspective.h b/src/plugins/axivion/axivionperspective.h index e2e7843e2a3..cc0d019afe0 100644 --- a/src/plugins/axivion/axivionperspective.h +++ b/src/plugins/axivion/axivionperspective.h @@ -15,5 +15,6 @@ void reinitDashboard(const QString &projectName); void resetDashboard(); void updateIssueDetails(const QString &html); void updatePerspectiveToolbar(); +void updateNamedFilters(); } // Axivion::Internal diff --git a/src/plugins/axivion/axivionplugin.cpp b/src/plugins/axivion/axivionplugin.cpp index 0dcc71d33f3..5683a648f67 100644 --- a/src/plugins/axivion/axivionplugin.cpp +++ b/src/plugins/axivion/axivionplugin.cpp @@ -155,7 +155,15 @@ static DashboardInfo toDashboardInfo(const GetDtoStorage 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 @@ -213,6 +221,7 @@ public: void handleIssuesForFile(const Dto::FileViewDto &fileView); void enableInlineIssues(bool enable); void fetchIssueInfo(const QString &id); + void fetchNamedFilters(); void onSessionLoaded(const QString &sessionName); void onAboutToSaveSession(); @@ -229,11 +238,14 @@ public: std::optional m_dashboardInfo; std::optional m_currentProjectInfo; std::optional m_analysisVersion; + QList m_globalNamedFilters; + QList m_userNamedFilters; Project *m_project = nullptr; bool m_runningQuery = false; TaskTreeRunner m_taskTreeRunner; std::unordered_map> m_docMarksTrees; TaskTreeRunner m_issueInfoRunner; + TaskTreeRunner m_namedFilterRunner; FileInProjectFinder m_fileFinder; // FIXME maybe obsolete when path mapping is implemented QMetaObject::Connection m_fileFinderConnection; QHash> m_allMarks; @@ -279,6 +291,46 @@ std::optional projectInfo() return dd->m_currentProjectInfo; } +void fetchNamedFilters() +{ + QTC_ASSERT(dd, return); + dd->fetchNamedFilters(); +} + +void knownNamedFilters(QList *global, QList *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 namedFilterInfoForKey(const QString &key, bool global) +{ + QTC_ASSERT(dd, return std::nullopt); + + const auto findFilter = [](const QList filters, const QString &key) + -> std::optional { + 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: move when curl is no more in use? bool handleCertificateIssue(const Utils::Id &serverId) @@ -388,6 +440,7 @@ static QByteArray contentTypeData(ContentType contentType) { switch (contentType) { case ContentType::Html: return s_htmlContentType; + case ContentType::Json: return s_jsonContentType; case ContentType::PlainText: return s_plaintextContentType; case ContentType::Svg: return s_svgContentType; } @@ -910,6 +963,62 @@ void AxivionPluginPrivate::fetchIssueInfo(const QString &id) }); } +static QList extractNamedFiltersFromJsonArray(const QByteArray &json) +{ + QList 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 globalStorage; + const Storage 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() { const QList openDocuments = DocumentModel::openedDocuments(); @@ -1086,7 +1195,10 @@ void switchActiveDashboardId(const Id &toDashboardId) dd->m_apiToken.reset(); dd->m_dashboardInfo.reset(); dd->m_currentProjectInfo.reset(); + dd->m_globalNamedFilters.clear(); + dd->m_userNamedFilters.clear(); updatePerspectiveToolbar(); + updateNamedFilters(); } const std::optional currentDashboardInfo() diff --git a/src/plugins/axivion/axivionplugin.h b/src/plugins/axivion/axivionplugin.h index bc3c2128414..c364a63a56d 100644 --- a/src/plugins/axivion/axivionplugin.h +++ b/src/plugins/axivion/axivionplugin.h @@ -62,10 +62,13 @@ public: QStringList projects; QHash projectUrls; std::optional checkCredentialsUrl; + std::optional globalNamedFilters; + std::optional userNamedFilters; }; enum class ContentType { Html, + Json, PlainText, Svg }; @@ -101,6 +104,18 @@ Tasking::Group lineMarkerRecipe(const Utils::FilePath &filePath, const LineMarke void fetchDashboardAndProjectInfo(const DashboardInfoHandler &handler, const QString &projectName); std::optional projectInfo(); + +struct NamedFilter +{ + QString key; + QString displayName; + bool global = false; +}; + +void fetchNamedFilters(); +void knownNamedFilters(QList *global, QList *user); +std::optional namedFilterInfoForKey(const QString &key, bool global); + bool handleCertificateIssue(); QIcon iconForIssue(const std::optional &issueKind); @@ -117,3 +132,4 @@ Utils::FilePath findFileForIssuePath(const Utils::FilePath &issuePath); } // Axivion::Internal +Q_DECLARE_METATYPE(Axivion::Internal::NamedFilter) diff --git a/src/plugins/axivion/issueheaderview.cpp b/src/plugins/axivion/issueheaderview.cpp index 9c2f4b68cd3..a293ca1eb2b 100644 --- a/src/plugins/axivion/issueheaderview.cpp +++ b/src/plugins/axivion/issueheaderview.cpp @@ -5,6 +5,7 @@ #include "axiviontr.h" +#include #include #include #include @@ -211,6 +212,51 @@ const QMap IssueHeaderView::currentFilterMapping() const return filter; } +void IssueHeaderView::updateExistingColumnInfos( + const std::map &filters, + const std::optional> &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) { if (event->button() == Qt::LeftButton) { diff --git a/src/plugins/axivion/issueheaderview.h b/src/plugins/axivion/issueheaderview.h index d95650d0e6d..0a000fce70c 100644 --- a/src/plugins/axivion/issueheaderview.h +++ b/src/plugins/axivion/issueheaderview.h @@ -3,6 +3,8 @@ #pragma once +#include "dashboard/dto.h" + #include #include @@ -30,6 +32,8 @@ public: const QString currentSortString() const; const QMap currentFilterMapping() const; + void updateExistingColumnInfos(const std::map &filters, + const std::optional> &sorters); signals: void filterChanged(); void sortTriggered();