├── Controller
└── Adminhtml
│ └── Index
│ └── Index.php
├── Model
├── QualityPatches.php
├── Source
│ └── PatchFilter.php
└── UpdateNotification.php
├── README.md
├── Ui
└── DataProvider
│ └── QualityPatchesProvider.php
├── composer.json
├── etc
├── acl.xml
├── adminhtml
│ ├── di.xml
│ ├── menu.xml
│ └── routes.xml
└── module.xml
├── registration.php
└── view
└── adminhtml
├── layout
└── aimes_patches_index_index.xml
├── templates
└── messages
│ └── update-available.phtml
└── ui_component
└── aimes_quality_patches_grid.xml
/Controller/Adminhtml/Index/Index.php:
--------------------------------------------------------------------------------
1 | resultPageFactory = $resultPageFactory;
40 | $this->updateNotification = $updateNotification;
41 | }
42 |
43 | public function execute()
44 | {
45 | $resultPage = $this->resultPageFactory->create();
46 |
47 | $this->updateNotification->addMessages();
48 |
49 | $resultPage->setActiveMenu('Aimes_QualityPatchesUi::quality_patches');
50 | $resultPage->getConfig()->getTitle()->prepend(__('Magento Quality Patches'));
51 |
52 | return $resultPage;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Model/QualityPatches.php:
--------------------------------------------------------------------------------
1 | containerFactory = $containerFactory;
49 | $this->filesystem = $filesystem;
50 | $this->json = $json;
51 | }
52 |
53 | /**
54 | * Get all patches for the current Magento/software version
55 | *
56 | * @return array
57 | */
58 | public function getAllPatches()
59 | {
60 | if ($this->patches !== null) {
61 | return $this->patches;
62 | }
63 |
64 | // Since the quality patches tool is outside the Magento application, we have to emulate the CLI command
65 | $container = $this->containerFactory->create([
66 | 'basePath' => $this->getCloudPatchesBaseDir(),
67 | 'magentoBasePath' => $this->getMagentoRootDir(),
68 | ]);
69 |
70 | $application = new Application($container);
71 |
72 | $input = new ArrayInput([
73 | 'command' => Status::NAME,
74 | '--format' => ShowStatus::FORMAT_JSON,
75 | ]);
76 | $input->setInteractive(false);
77 |
78 | $output = new BufferedOutput();
79 |
80 | try {
81 | $application->get(Status::NAME)->run($input, $output);
82 | $patchInfo = $this->json->unserialize($output->fetch());
83 |
84 | // Fix the seemingly random newline characters breaking up words
85 | foreach ($patchInfo as &$patch) {
86 | $patch['Title'] = $this->removeNewlines($patch['Title']);
87 | }
88 |
89 | $this->patches = $patchInfo;
90 | } catch (ExceptionInterface $exception) {
91 | $this->patches = [];
92 | }
93 |
94 | return $this->patches;
95 | }
96 |
97 | /**
98 | * Get patch by name/ID for the current Magento/software version
99 | *
100 | * @param string $id
101 | *
102 | * @return array|null
103 | */
104 | public function getPatchById(string $id): ?array
105 | {
106 | $patches = $this->getAllPatches();
107 |
108 | foreach ($patches as $key => $patch) {
109 | if ($patch['Id'] === $id) {
110 | return $patches[$key];
111 | }
112 | }
113 |
114 | return null;
115 | }
116 |
117 | /**
118 | * Get root path of the magento cloud patches package
119 | *
120 | * @return string
121 | */
122 | private function getCloudPatchesBaseDir(): string
123 | {
124 | $applicationReflection = new ReflectionClass(Application::class);
125 | $filepath = $applicationReflection->getFileName();
126 |
127 | return dirname($filepath, 2);
128 | }
129 |
130 | /**
131 | * Get Magento root directory
132 | *
133 | * @return string
134 | */
135 | private function getMagentoRootDir(): string
136 | {
137 | return $this->filesystem->getDirectoryRead(DirectoryList::ROOT)->getAbsolutePath();
138 | }
139 |
140 | /**
141 | * Remove newline characters from the JSON output
142 | *
143 | * @param string $string
144 | *
145 | * @return string
146 | */
147 | private function removeNewlines(string $string): string
148 | {
149 | return str_replace("\n", '', $string);
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/Model/Source/PatchFilter.php:
--------------------------------------------------------------------------------
1 | qualityPatches = $qualityPatches;
30 | $this->filterKey = $filterKey;
31 | }
32 |
33 | /**
34 | * Get statuses available from patch list
35 | *
36 | * @return array
37 | */
38 | public function toOptionArray(): array
39 | {
40 | $patches = $this->qualityPatches->getAllPatches();
41 | $options = [];
42 | $values = [];
43 |
44 | foreach ($patches as $patch) {
45 | $values[] = $patch[$this->filterKey];
46 | }
47 |
48 | $values = array_unique($values);
49 |
50 | foreach ($values as $value) {
51 | $options[] = [
52 | 'label' => $value,
53 | 'value' => $value,
54 | ];
55 | }
56 |
57 | return $options;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Model/UpdateNotification.php:
--------------------------------------------------------------------------------
1 | 'https://experienceleague.adobe.com/docs/commerce-cloud-service/user-guide/release-notes/cloud-patches.html',
29 | 'magento/quality-patches' => 'https://experienceleague.adobe.com/docs/commerce-operations/tools/quality-patches-tool/release-notes.html',
30 | ];
31 |
32 | foreach ($packageReleaseNotesMapping as $packageName => $releaseNotesUrl) {
33 | $isUpdateAvailable = $this->composerVersioning->isUpdateAvailable($packageName);
34 |
35 | if (!$isUpdateAvailable) {
36 | continue;
37 | }
38 |
39 | $packageInfo = $this->composerVersioning->getPackageInfo($packageName);
40 | $packageInfo['release_notes_url'] = $releaseNotesUrl;
41 |
42 | $this->messageManager->addComplexNoticeMessage(
43 | 'qualityPatchesUpdateNotification',
44 | $packageInfo
45 | );
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Aimes_QualityPatchesUi
2 | !["Supported Magento Version"][magento-badge] !["Latest Release"][release-badge]
3 |
4 | * Compatible with _Magento Open Source_ and _Adobe Commerce_ `2.4.x`
5 |
6 | ## Features
7 | - Display Magento Quality Patch Status as a grid, without need for CLI access, in the admin panel
8 | - Notify users of any updates available to the following packages (when viewing the grid), as a new version may contain new patches:
9 | - `magento/magento-cloud-patches`
10 | - `magento/quality-patches`
11 |
12 | ## Requirements
13 | * Magento Open Source or Adobe Commerce version `2.4.x`
14 |
15 | ## Installation
16 | Please install this module via Composer. This module is hosted on [Packagist][packagist].
17 |
18 | * `composer require aimes/module-quality-patches-ui`
19 | * `bin/magento module:enable Aimes_QualityPatchesUi`
20 | * `bin/magento setup:upgrade`
21 |
22 | ## Usage
23 | Navigate to `Reports -> Patch Status -> Quality Patches` in the admin area
24 |
25 | ## Preview
26 | 
27 | 
28 |
29 |
30 | ## Licence
31 | [GPLv3][gpl] © [Rob Aimes][author]
32 |
33 | [magento-badge]:https://img.shields.io/badge/magento-2.3.x%20%7C%202.4.x-orange.svg?logo=magento&style=for-the-badge
34 | [release-badge]:https://img.shields.io/github/v/release/robaimes/module-quality-patches-ui
35 | [packagist]:https://packagist.org/packages/aimes/module-quality-patches-ui
36 | [gpl]:https://www.gnu.org/licenses/gpl-3.0.en.html
37 | [author]:https://aimes.dev/
38 | [composer-patches]:https://github.com/cweagans/composer-patches
39 |
--------------------------------------------------------------------------------
/Ui/DataProvider/QualityPatchesProvider.php:
--------------------------------------------------------------------------------
1 | qualityPatches = $qualityPatches;
65 | }
66 |
67 | /**
68 | * Get 'static' data from Magento cloud patches output
69 | *
70 | * Pagination, sorting and filtering is normally done via collection.
71 | *
72 | * @return array
73 | */
74 | public function getData(): array
75 | {
76 | $patches = $this->patchData;
77 |
78 | if (!$patches) {
79 | $this->patchData = $patches = $this->qualityPatches->getAllPatches();
80 | }
81 |
82 | $searchCriteria = $this->getSearchCriteria();
83 |
84 | $this->filterResults($patches, $searchCriteria);
85 | $this->sortResults($patches, $searchCriteria);
86 |
87 | return [
88 | 'totalRecords' => count($patches),
89 | 'items' => $this->getPaginationItems($patches, $searchCriteria),
90 | ];
91 | }
92 |
93 | /**
94 | * Filter results manually as we have no 'resource' or database
95 | *
96 | * @param array $results
97 | * @param SearchCriteria $searchCriteria
98 | *
99 | * @return void
100 | * @see Collection::addFieldToFilter
101 | */
102 | private function filterResults(array &$results, SearchCriteria $searchCriteria): void
103 | {
104 | $filterGroups = $searchCriteria->getFilterGroups();
105 | $filters = [];
106 |
107 | // This grid is assumed to match other grid components and as such functions as logical AND when filtering
108 | foreach ($filterGroups as $filterGroup) {
109 | foreach ($filterGroup->getFilters() as $filter) {
110 | $filters[] = $filter;
111 | }
112 | }
113 |
114 | /** @var Filter[] $filters */
115 | foreach ($filters as $filter) {
116 | foreach ($results as $index => $result) {
117 | if (!$result[$filter->getField()]) {
118 | unset($results[$index]);
119 | }
120 |
121 | $filterValue = strtolower($filter->getValue());
122 | $dataValue = strtolower($result[$filter->getField()]);
123 |
124 | switch ($filter->getConditionType()) {
125 | case 'eq':
126 | if ($dataValue === $filterValue) {
127 | continue 2;
128 | }
129 |
130 | break;
131 | case 'like':
132 | if (strpos($dataValue, trim($filterValue, '%')) !== false) {
133 | continue 2;
134 | }
135 |
136 | break;
137 | default:
138 | break;
139 | }
140 |
141 | unset($results[$index]);
142 | }
143 | }
144 | }
145 |
146 | /**
147 | * Sort grid results manually as we have no 'resource' or database
148 | *
149 | * @param array $results
150 | * @param SearchCriteria $searchCriteria
151 | *
152 | * @return void
153 | */
154 | private function sortResults(array &$results, SearchCriteria $searchCriteria): void
155 | {
156 | $sortOrders = $searchCriteria->getSortOrders();
157 |
158 | foreach ($sortOrders as $sortOrder) {
159 | if (!$sortOrder->getField()) {
160 | continue;
161 | }
162 |
163 | usort($results, function ($itemA, $itemB) use ($sortOrder) {
164 | $comparisonFieldA = $itemA[$sortOrder->getField()];
165 | $comparisonFieldB = $itemB[$sortOrder->getField()];
166 |
167 | if (strtoupper($sortOrder->getDirection()) === Collection::SORT_ORDER_ASC) {
168 | return $comparisonFieldA <=> $comparisonFieldB;
169 | } else {
170 | return $comparisonFieldB <=> $comparisonFieldA;
171 | }
172 | });
173 | }
174 | }
175 |
176 | /**
177 | * Paginate results manually as we have no 'resource' or database
178 | *
179 | * @param array $items
180 | * @param SearchCriteria $searchCriteria
181 | *
182 | * @return array
183 | */
184 | private function getPaginationItems(array $items, SearchCriteria $searchCriteria): array
185 | {
186 | $pageSize = $searchCriteria->getPageSize();
187 | $pageCurrent = $searchCriteria->getCurrentPage();
188 | $pageOffset = ($pageCurrent - 1) * $pageSize;
189 |
190 | return array_slice($items, $pageOffset, $pageSize);
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aimes/module-quality-patches-ui",
3 | "type": "magento2-module",
4 | "description": "Admin panel GUI for reporting status of Magento Quality Patches",
5 | "license": [
6 | "GPL-3.0-or-later"
7 | ],
8 | "authors": [
9 | {
10 | "name": "Rob Aimes",
11 | "email": "rob@aimes.dev",
12 | "homepage": "https://aimes.dev"
13 | }
14 | ],
15 | "require": {
16 | "aimes/magento2-module-substratum": "^1.0",
17 | "magento/magento-cloud-patches": "^1.0.20",
18 | "magento/quality-patches": "^1.1"
19 | },
20 | "autoload": {
21 | "psr-4": {
22 | "Aimes\\QualityPatchesUi\\": ""
23 | },
24 | "files": [
25 | "registration.php"
26 | ]
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/etc/acl.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
%package_name
package is available. Additional patches may be available.',
17 | [
18 | 'package_name' => $block->getName(),
19 | ]
20 | ), ['code']); ?>
21 |
22 | %current_version
and the latest version available is %latest_version
.',
27 | [
28 | 'current_version' => $block->getCurrentVersion(),
29 | 'latest_version' => $block->getLatest(),
30 | ]
31 | ), ['code']); ?>
32 |
33 |