/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #pragma once #include <AWSResourceManager.h> #include <AWSUtil.h> #include <QTime> #include <QDebug> #include <QSortFilterProxyModel> #include <IEditor.h> class StackResourcesModel : public IStackResourcesModel { Q_OBJECT public: StackResourcesModel(AWSResourceManager* resourceManager) : m_resourceManager{resourceManager} , m_requestId{resourceManager->AllocateRequestId()} { InitializeModel(this); connect(m_resourceManager, &AWSResourceManager::ProcessOutputResourceList, this, &StackResourcesModel::ProcessOutputResourceList); appendEmptyRow(); } void Refresh(bool force) override { if (force || IsRefreshTime()) { QVariantMap args; const char* command = PrepareRefreshCommand(args); m_resourceManager->ExecuteAsyncWithRetry(m_requestId, command, args); m_isRefreshing = true; RefreshStatusChanged(m_isRefreshing); } } QString GetStackResourceRegion() const override { QSharedPointer<IAWSDeploymentModel> deploymentModel = m_resourceManager->GetDeploymentModel(); QString region = deploymentModel->GetActiveDeploymentRegion(); return region; } virtual const char* PrepareRefreshCommand(QVariantMap& args) = 0; bool IsRefreshTime() { return !m_lastRefreshTime.isValid() || m_lastRefreshTime.elapsed() > 2000; } bool IsRefreshing() const override { return m_isRefreshing; } bool IsReady() const override { return m_isReady; } QVariant headerData(int section, Qt::Orientation orientation, int role) const override { if (orientation == Qt::Horizontal) { if (role == Qt::TextAlignmentRole) { return Qt::AlignLeft; } } return IStackResourcesModel::headerData(section, orientation, role); } private: AWSResourceManager* m_resourceManager; bool m_isRefreshing { false }; bool m_isReady { false }; QElapsedTimer m_lastRefreshTime {}; AWSResourceManager::RequestId m_requestId; bool m_containsChanges{ false }; bool m_containsDeletionChanges{ false }; bool m_containsSecurityChanges{ false }; static void InitializeModel(QStandardItemModel* model) { model->setColumnCount(ColumnCount); model->setHorizontalHeaderItem(PendingActionColumn, new QStandardItem{ "Pending" }); model->setHorizontalHeaderItem(ChangeImpactColumn, new QStandardItem{ "Impacts" }); model->setHorizontalHeaderItem(NameColumn, new QStandardItem{ "Resource Name" }); model->setHorizontalHeaderItem(ResourceTypeColumn, new QStandardItem{ "Type" }); model->setHorizontalHeaderItem(ResourceStatusColumn, new QStandardItem{ "Status" }); model->setHorizontalHeaderItem(TimestampColumn, new QStandardItem{ "Timestamp" }); model->setHorizontalHeaderItem(PhysicalResourceIdColumn, new QStandardItem{ "ID" }); } void ProcessOutputResourceList(AWSResourceManager::RequestId requestId, const QVariant& value) { if (requestId != m_requestId) { return; } auto map = value.toMap(); auto resources = VariantListToMapList(map["Resources"].toList()); Sort(resources, "Name"); beginResetModel(); m_containsChanges = false; m_containsSecurityChanges = false; m_containsDeletionChanges = false; removeRows(0, rowCount()); for (auto it = resources.constBegin(); it != resources.constEnd(); ++it) { appendRow(MakeRow(*it)); } if (rowCount() == 0) { appendEmptyRow(); } m_lastRefreshTime.start(); m_isRefreshing = false; m_isReady = true; endResetModel(); RefreshStatusChanged(m_isRefreshing); } void appendEmptyRow() { QVariantMap map; appendRow(MakeRow(map)); } static QList<QVariantMap> VariantListToMapList(const QList<QVariant>& list) { QList<QVariantMap> result; for (auto it = list.constBegin(); it != list.constEnd(); ++it) { result.append(it->toMap()); } return result; } static void Sort(QList<QVariantMap>& list, const QString& column) { auto compare = [column](const QVariantMap& v1, const QVariantMap& v2) { return v1[column].toString() < v2[column].toString(); }; std::sort(list.begin(), list.end(), compare); } enum { PendingChangeFilterRole = Qt::UserRole }; QList<QStandardItem*> MakeRow(const QVariantMap& resource) { QList<QStandardItem*> row {}; for (int column = 0; column < ColumnCount; ++column) { row.append(new QStandardItem {}); } auto pendingAction = resource["PendingAction"].toString(); if (pendingAction.isEmpty()) { row[PendingActionColumn]->setText("--"); row[PendingActionColumn]->setData("NO", PendingChangeFilterRole); } else { row[PendingActionColumn]->setText(AWSUtil::MakePrettyPendingActionText(pendingAction)); row[PendingActionColumn]->setData(AWSUtil::MakePrettyPendingReasonTooltip(resource["PendingReason"].toString()), Qt::ToolTipRole); row[PendingActionColumn]->setData(AWSUtil::MakePrettyPendingActionColor(pendingAction), Qt::ForegroundRole); row[PendingActionColumn]->setData("YES", PendingChangeFilterRole); m_containsChanges = true; if (pendingAction == "DELETE") { m_containsDeletionChanges = true; } } auto isPendingSecurityChange = resource["IsPendingSecurityChange"].toBool(); if (isPendingSecurityChange) { row[ChangeImpactColumn]->setText(tr("Security")); row[ChangeImpactColumn]->setData(AWSUtil::MakePrettyPendingReasonTooltip(resource["PendingReason"].toString()), Qt::ToolTipRole); row[ChangeImpactColumn]->setData(AWSUtil::MakePrettyColor("Default"), Qt::ForegroundRole); m_containsSecurityChanges = true; } else { row[ChangeImpactColumn]->setText("--"); } auto name = resource["Name"].toString(); if (name.isEmpty()) { name = "--"; } row[NameColumn]->setText(name); auto type = resource["ResourceType"].toString(); if (type.isEmpty()) { type = "--"; } row[ResourceTypeColumn]->setText(type); auto resourceStatus = resource["ResourceStatus"].toString(); if (resourceStatus.isEmpty()) { row[ResourceStatusColumn]->setText("--"); } else { row[ResourceStatusColumn]->setText(AWSUtil::MakePrettyResourceStatusText(resourceStatus)); row[ResourceStatusColumn]->setData(AWSUtil::MakePrettyResourceStatusTooltip(resourceStatus, resource["ResourceStatusReason"].toString()), Qt::ToolTipRole); row[ResourceStatusColumn]->setData(AWSUtil::MakePrettyResourceStatusColor(resourceStatus), Qt::ForegroundRole); } auto timestamp = resource["Timestamp"].toDateTime().toLocalTime().toString(); if (timestamp.isEmpty()) { timestamp = "--"; } row[TimestampColumn]->setText(timestamp); auto id = resource["PhysicalResourceId"].toString(); if (id.isEmpty()) { row[PhysicalResourceIdColumn]->setText("--"); } else { row[PhysicalResourceIdColumn]->setText(AWSUtil::MakeShortResourceId(id)); row[PhysicalResourceIdColumn]->setData(id, Qt::ToolTipRole); } return row; } // This is... convoluted... when prompting the user to confirm changes we want to // show an empty row while waiting for the results of a forced refresh, then if // there are no changes, we want to continue showing an empty row. // // If we go with the QSortFilterProxyModel approach, we need a way to fake this // empty row. We do that by starting out as a proxy for an "empty" model, which // contains only the empty row, and then switching to be a proxy of the real // model once it is reset, but only if it contains changes. // // We also depend on the view to release the proxy before releasing the underlying // model. If we had a way to get from "this" to a QSmartPointer that would not // be a problem. class EmptyPendingChangeProxyModel : public QStandardItemModel { public: EmptyPendingChangeProxyModel() { InitializeModel(this); QList<QStandardItem*> row{}; for (int column = 0; column < ColumnCount; ++column) { row.append(new QStandardItem{}); } row[PendingActionColumn]->setText("--"); row[PendingActionColumn]->setData("YES", PendingChangeFilterRole); row[ChangeImpactColumn]->setText("--"); row[NameColumn]->setText("--"); row[ResourceTypeColumn]->setText("--"); row[ResourceStatusColumn]->setText("--"); row[TimestampColumn]->setText("--"); row[PhysicalResourceIdColumn]->setText("--"); appendRow(row); } QVariant headerData(int section, Qt::Orientation orientation, int role) const override { if (orientation == Qt::Horizontal) { if (role == Qt::TextAlignmentRole) { return Qt::AlignLeft; } } return QStandardItemModel::headerData(section, orientation, role); } }; EmptyPendingChangeProxyModel m_emptyPendingChangeProxyModel; EmptyPendingChangeProxyModel* GetEmptyPendingChangeProxyModel() { return &m_emptyPendingChangeProxyModel; } class PendingChangesProxyModel : public QSortFilterProxyModel { public: PendingChangesProxyModel(StackResourcesModel* proxiedModel) : QSortFilterProxyModel{ proxiedModel } , m_proxiedModel{ proxiedModel } { setFilterKeyColumn(PendingActionColumn); setFilterRole(PendingChangeFilterRole); setFilterFixedString("YES"); setSourceModel(m_proxiedModel->GetEmptyPendingChangeProxyModel()); connect(m_proxiedModel, &StackResourcesModel::modelReset, this, &PendingChangesProxyModel::OnProxiedModelReset); m_proxiedModel->Refresh(true); // force } private: StackResourcesModel* m_proxiedModel; void OnProxiedModelReset() { // we depend on setSourceModel to fire the modelReset signal for the view if (m_proxiedModel->ContainsChanges()) { setSourceModel(m_proxiedModel); } else { setSourceModel(m_proxiedModel->GetEmptyPendingChangeProxyModel()); } } }; QSharedPointer<QSortFilterProxyModel> GetPendingChangeFilterProxy() override { return QSharedPointer<PendingChangesProxyModel>::create(this).dynamicCast<QSortFilterProxyModel>(); } bool ContainsChanges() override { return m_containsChanges; } bool ContainsDeletionChanges() { return m_containsDeletionChanges; } bool ContainsSecurityChanges() { return m_containsSecurityChanges; } };