Skip to content

Commit 71ca796

Browse files
authored
Merge pull request #7802 from magento-performance/ACPT-677
[Performance] Catalog product grid limitation
2 parents e235082 + 6fddf5d commit 71ca796

File tree

21 files changed

+510
-21
lines changed

21 files changed

+510
-21
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Backend\ViewModel;
9+
10+
use Magento\Framework\App\Config\ScopeConfigInterface;
11+
use Magento\Framework\View\Element\Block\ArgumentInterface;
12+
13+
/**
14+
* Get configuration values related to limit total number of products in grid collection.
15+
*/
16+
class LimitTotalNumberOfProductsInGrid implements ArgumentInterface
17+
{
18+
/**
19+
* @var ScopeConfigInterface
20+
*/
21+
private ScopeConfigInterface $scopeConfig;
22+
23+
/**
24+
* @param ScopeConfigInterface $scopeConfig
25+
*/
26+
public function __construct(
27+
ScopeConfigInterface $scopeConfig
28+
) {
29+
$this->scopeConfig = $scopeConfig;
30+
}
31+
32+
/**
33+
* Check if configuration setting to limit total number of products in grid is enabled.
34+
*
35+
* @return bool
36+
*/
37+
public function limitTotalNumberOfProducts(): bool
38+
{
39+
return (bool)$this->scopeConfig->getValue('admin/grid/limit_total_number_of_products');
40+
}
41+
42+
/**
43+
* Get records threshold for limit total number of products in collection.
44+
*
45+
* @return int
46+
*/
47+
public function getRecordsLimit(): int
48+
{
49+
return (int)$this->scopeConfig->getValue('admin/grid/records_limit');
50+
}
51+
}

app/code/Magento/Backend/etc/adminhtml/system.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,23 @@
480480
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
481481
</field>
482482
</group>
483+
<group id="grid" translate="label" type="text" sortOrder="45" showInDefault="1">
484+
<label>Admin Grids</label>
485+
<field id="limit_total_number_of_products" translate="label comment"
486+
type="select" sortOrder="1" showInDefault="1" canRestore="1">
487+
<label>Limit Number of Products in Grid</label>
488+
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
489+
<comment>Limit total number of products in grid collection.</comment>
490+
</field>
491+
<field id="records_limit" translate="label comment" type="text" sortOrder="2" showInDefault="1" canRestore="1">
492+
<label>Records Limit</label>
493+
<validate>validate-digits validate-digits-range digits-range-20000-</validate>
494+
<comment>Limit total number of products in grid collection if their number is greater than this value. Minimum value: 20000.</comment>
495+
<depends>
496+
<field id="limit_total_number_of_products">1</field>
497+
</depends>
498+
</field>
499+
</group>
483500
</section>
484501
<section id="web" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
485502
<label>Web</label>

app/code/Magento/Backend/etc/config.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@
3131
<max_height>1200</max_height>
3232
</upload_configuration>
3333
</system>
34+
<admin>
35+
<grid>
36+
<limit_total_number_of_products>0</limit_total_number_of_products>
37+
<records_limit>20000</records_limit>
38+
</grid>
39+
</admin>
3440
<general>
3541
<validator_data>
3642
<input_types>

app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
* Copyright © Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
6+
7+
use Magento\Catalog\Ui\DataProvider\Product\ProductCollection;
8+
69
?>
710
<?php
811
/**
@@ -18,8 +21,10 @@
1821
/**
1922
* @var \Magento\Backend\Block\Widget\Grid\Extended $block
2023
* @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer
24+
* @var \Magento\Backend\ViewModel\LimitTotalNumberOfProductsInGrid $viewModel
2125
*/
2226
$numColumns = count($block->getColumns());
27+
$viewModel = $block->getViewModel();
2328

2429
?>
2530
<?php if ($block->getCollection()): ?>
@@ -68,11 +73,15 @@ $numColumns = count($block->getColumns());
6873
<?php endif; ?>
6974
<?php $countRecords = $block->getCollection()->getSize(); ?>
7075
<div class="admin__control-support-text">
76+
<?php if (!$block->getCollection() instanceof ProductCollection
77+
|| !$viewModel->limitTotalNumberOfProducts()
78+
|| $countRecords < $viewModel->getRecordsLimit()): ?>
7179
<span id="<?= $block->escapeHtml($block->getHtmlId()) ?>-total-count"
7280
<?= /* @noEscape */ $block->getUiId('total-count') ?>>
7381
<?= /* @noEscape */ $countRecords ?>
7482
</span>
75-
<?= $block->escapeHtml(__('records found')) ?>
83+
<?= $block->escapeHtml(__('records found')) ?>
84+
<?php endif; ?>
7685
<span id="<?= $block->escapeHtml($block->getHtmlId()) ?>_massaction-count"
7786
class="mass-select-info _empty">
7887
<strong data-role="counter">0</strong>
@@ -124,23 +133,27 @@ $numColumns = count($block->getColumns());
124133
<span><?= $block->escapeHtml(__('Previous page')) ?></span>
125134
</button>
126135
<?php endif; ?>
127-
<input type="text"
128-
id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current"
129-
name="<?= $block->escapeHtmlAttr($block->getVarNamePage()) ?>"
130-
value="<?= $block->escapeHtmlAttr($_curPage) ?>"
131-
class="admin__control-text"
132-
<?= /* @noEscape */ $block->getUiId('current-page') ?> />
133-
<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag(
134-
'onkeypress',
135-
/* @noEscape */ $block->getJsObjectName() . '.inputPage(event, \'' .
136-
/* @noEscape */ $_lastPage . '\')',
137-
'#' . $block->escapeHtml($block->getHtmlId()) . '_page-current'
138-
) ?>
139-
<label class="admin__control-support-text"
140-
for="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current">
141-
<?= /* @noEscape */ __('of %1', '<span>' .
142-
$block->getCollection()->getLastPageNumber() . '</span>') ?>
143-
</label>
136+
<?php if (!$block->getCollection() instanceof ProductCollection
137+
|| !$viewModel->limitTotalNumberOfProducts()
138+
|| $countRecords < $viewModel->getRecordsLimit()): ?>
139+
<input type="text"
140+
id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current"
141+
name="<?= $block->escapeHtmlAttr($block->getVarNamePage()) ?>"
142+
value="<?= $block->escapeHtmlAttr($_curPage) ?>"
143+
class="admin__control-text"
144+
<?= /* @noEscape */ $block->getUiId('current-page') ?> />
145+
<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag(
146+
'onkeypress',
147+
/* @noEscape */ $block->getJsObjectName() . '.inputPage(event, \'' .
148+
/* @noEscape */ $_lastPage . '\')',
149+
'#' . $block->escapeHtml($block->getHtmlId()) . '_page-current'
150+
) ?>
151+
<label class="admin__control-support-text"
152+
for="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current">
153+
<?= /* @noEscape */ __('of %1', '<span>' .
154+
$block->getCollection()->getLastPageNumber() . '</span>') ?>
155+
</label>
156+
<?php endif; ?>
144157
<?php if ($_curPage < $_lastPage): ?>
145158
<button type="button"
146159
title="<?= $block->escapeHtmlAttr(__('Next page')) ?>"
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Catalog\Plugin\Ui\DataProvider\Product;
10+
11+
use Magento\Catalog\Ui\DataProvider\Product\ProductDataProvider as CatalogProductDataProvider;
12+
use Magento\Framework\App\Config\ScopeConfigInterface;
13+
14+
/**
15+
* Modify catalog product UI data with show total records flag.
16+
*/
17+
class ProductDataProvider
18+
{
19+
/**
20+
* @var ScopeConfigInterface
21+
*/
22+
private ScopeConfigInterface $scopeConfig;
23+
24+
/**
25+
* @param ScopeConfigInterface $scopeConfig
26+
*/
27+
public function __construct(ScopeConfigInterface $scopeConfig)
28+
{
29+
$this->scopeConfig = $scopeConfig;
30+
}
31+
32+
/**
33+
* Modify catalog product UI data with show total records flag.
34+
*
35+
* @param CatalogProductDataProvider $subject
36+
* @param array $data
37+
* @return array
38+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
39+
*/
40+
public function afterGetData(CatalogProductDataProvider $subject, array $data): array
41+
{
42+
return $this->addShowTotalRecords($data);
43+
}
44+
45+
/**
46+
* Add flag to display/hide total records found and pagination elements in products grid header.
47+
*
48+
* @param array $data
49+
* @return array
50+
*/
51+
private function addShowTotalRecords(array $data): array
52+
{
53+
if (key_exists('totalRecords', $data)) {
54+
if ($this->scopeConfig->getValue('admin/grid/limit_total_number_of_products')
55+
&& $data['totalRecords'] >= $this->scopeConfig->getValue('admin/grid/records_limit')) {
56+
$data['showTotalRecords'] = false;
57+
} else {
58+
$data['showTotalRecords'] = true;
59+
}
60+
}
61+
62+
return $data;
63+
}
64+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
9+
<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
11+
<test name="AdminLimitNumberOfProductsInGridTest">
12+
<annotations>
13+
<features value="Catalog"/>
14+
<stories value="Limit number of products in grid"/>
15+
<title value="Limit total number of products in grid collection"/>
16+
<description value="Verify that total number of products in grid collection can be limited"/>
17+
<severity value="MAJOR"/>
18+
<testCaseId value="ACPT-699"/>
19+
<group value="Catalog"/>
20+
</annotations>
21+
22+
<before>
23+
<magentoCLI stepKey="enableLimitNumberOfProductsInGrid" command="config:set admin/grid/limit_total_number_of_products 1"/>
24+
<magentoCLI stepKey="setCustomRecordsLimit" command="config:set admin/grid/records_limit 2"/>
25+
<createData entity="SimpleProduct" stepKey="createSimpleProduct1"/>
26+
<actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/>
27+
</before>
28+
29+
<after>
30+
<magentoCLI stepKey="disableLimitNumberOfProductsInGrid" command="config:set admin/grid/limit_total_number_of_products 0"/>
31+
<magentoCLI stepKey="setDefaultRecordsLimit" command="config:set admin/grid/records_limit 20000"/>
32+
<deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/>
33+
<deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/>
34+
<deleteData createDataKey="createSimpleProduct3" stepKey="deleteSimpleProduct3"/>
35+
<actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/>
36+
<actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/>
37+
</after>
38+
39+
<!-- Check that 1 record found and pagination is displayed because number of products is less than limit -->
40+
<actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/>
41+
<actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assert1RecordInProductGrid">
42+
<argument name="number" value="1"/>
43+
</actionGroup>
44+
<see selector="{{AdminGridHeaders.numberOfPages}}" userInput="of 1" stepKey="see1PageFound"/>
45+
46+
<!-- Create 2 more products -->
47+
<createData entity="SimpleProduct2" stepKey="createSimpleProduct2"/>
48+
<createData entity="SimpleProduct3" stepKey="createSimpleProduct3"/>
49+
<!-- Check that 3 records found and pagination is not displayed because number of products is greater than limit -->
50+
<actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter2"/>
51+
<dontSee userInput="3 records found" selector="{{AdminGridHeaders.totalRecords}}" stepKey="dontSeeRecords"/>
52+
<dontSee selector="{{AdminGridHeaders.numberOfPages}}" userInput="of 1" stepKey="dontSee1PageFound"/>
53+
54+
<!-- Search for 1 simple product by name in filter -->
55+
<actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterSimpleProductByName">
56+
<argument name="product" value="SimpleProduct3"/>
57+
</actionGroup>
58+
<!-- Check that 1 record found and pagination is displayed because number of filtered products is less than limit -->
59+
<actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertOneRecordInProductGrid">
60+
<argument name="number" value="1"/>
61+
</actionGroup>
62+
<see selector="{{AdminGridHeaders.numberOfPages}}" userInput="of 1" stepKey="see1PageFound2"/>
63+
64+
<!-- Search for all 3 simple products in filter -->
65+
<conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/>
66+
<click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/>
67+
<fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="Simple Product" stepKey="fillProductNameFilter"/>
68+
<click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/>
69+
<waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad" time="30"/>
70+
<!-- Check that 3 records found and pagination is not displayed because number of filtered products is greater than limit -->
71+
<dontSee userInput="3 records found" selector="{{AdminGridHeaders.totalRecords}}" stepKey="dontSeeThreeRecords"/>
72+
<dontSee selector="{{AdminGridHeaders.numberOfPages}}" userInput="of 1" stepKey="dontSee1PageFound2"/>
73+
</test>
74+
</tests>

app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66
namespace Magento\Catalog\Ui\DataProvider\Product;
77

8+
use Magento\Framework\DB\Select;
9+
810
/**
911
* Collection which is used for rendering product list in the backend.
1012
*
@@ -25,4 +27,52 @@ protected function _productLimitationJoinPrice()
2527
$this->_productLimitationFilters->setUsePriceIndex(false);
2628
return $this->_productLimitationPrice(true);
2729
}
30+
31+
/**
32+
* @inheritdoc
33+
*/
34+
public function getSize()
35+
{
36+
if ($this->_totalRecords === null) {
37+
if ($this->_scopeConfig->getValue('admin/grid/limit_total_number_of_products')) {
38+
$sql = $this->getSelectCountSql();
39+
$estimatedRowsCount = $this->analyzeRows($sql);
40+
$recordsLimit = $this->_scopeConfig->getValue('admin/grid/records_limit');
41+
42+
if ($estimatedRowsCount > $recordsLimit) {
43+
$columns = $sql->getPart(Select::COLUMNS);
44+
$sql->reset(Select::COLUMNS);
45+
46+
foreach ($columns as &$column) {
47+
if ($column[1] instanceof \Zend_Db_Expr && $column[1] == "COUNT(DISTINCT e.entity_id)") {
48+
$column[1] = new \Zend_Db_Expr('e.entity_id');
49+
}
50+
}
51+
$sql->setPart(Select::COLUMNS, $columns);
52+
$sql->limit($recordsLimit);
53+
$query = new \Zend_Db_Expr('SELECT COUNT(*) FROM (' . $sql->assemble() . ') AS c');
54+
$this->_totalRecords = (int)$this->getConnection()->query($query)->fetchColumn();
55+
} else {
56+
return parent::getSize();
57+
}
58+
return $this->_totalRecords;
59+
}
60+
return parent::getSize();
61+
}
62+
return $this->_totalRecords;
63+
}
64+
65+
/**
66+
* Analyze number of rows to be examined to execute the query.
67+
*
68+
* @param Select $sql
69+
* @return mixed
70+
* @throws \Zend_Db_Statement_Exception
71+
*/
72+
private function analyzeRows(Select $sql)
73+
{
74+
$results = $this->getConnection()->query('EXPLAIN ' . $sql)->fetchAll();
75+
76+
return max(array_column($results, 'rows'));
77+
}
2878
}

app/code/Magento/Catalog/etc/adminhtml/di.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
<argument name="collectionFactory" xsi:type="object">\Magento\Catalog\Ui\DataProvider\Product\ProductCollectionFactory</argument>
9595
<argument name="modifiersPool" xsi:type="object">Magento\Catalog\Ui\DataProvider\Product\Listing\Modifier\Pool</argument>
9696
</arguments>
97+
<plugin name="add_show_total_records_to_catalog_product_data" type="Magento\Catalog\Plugin\Ui\DataProvider\Product\ProductDataProvider"/>
9798
</type>
9899
<type name="Magento\Catalog\Model\Product\Action">
99100
<plugin name="invalidate_pagecache_after_update_product_attributes" type="Magento\Catalog\Plugin\Model\Product\Action\UpdateAttributesFlushCache"/>

app/code/Magento/Sales/etc/adminhtml/di.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,9 @@
5252
<type name="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult">
5353
<plugin name="orderGridCollectionFilterPlugin" type="Magento\Sales\Plugin\Model\ResourceModel\Order\OrderGridCollectionFilter"/>
5454
</type>
55+
<type name="Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider\ProductCollection">
56+
<arguments>
57+
<argument name="collectionFactory" xsi:type="object">\Magento\Catalog\Ui\DataProvider\Product\ProductCollectionFactory</argument>
58+
</arguments>
59+
</type>
5560
</config>

0 commit comments

Comments
 (0)