Repository: mirasvit/module-profiler Branch: master Commit: 874c2d85d18a Files: 53 Total size: 144.5 KB Directory structure: gitextract_tcnu8bym/ ├── LICENSE.txt ├── README.md ├── composer.json ├── registration.php └── src/ └── Profiler/ ├── Api/ │ └── Data/ │ └── ProfileInterface.php ├── Block/ │ ├── Context.php │ ├── Profile/ │ │ ├── Listing.php │ │ └── View.php │ ├── Tab/ │ │ ├── Code.php │ │ ├── Database.php │ │ ├── IO.php │ │ └── TabInterface.php │ └── Toolbar.php ├── Console/ │ └── Command/ │ ├── AbstractCommand.php │ ├── AllowIpsCommand.php │ ├── DisableCommand.php │ ├── EnableCommand.php │ └── StatusCommand.php ├── Controller/ │ ├── Profile/ │ │ ├── Index.php │ │ └── View.php │ └── Profile.php ├── Helper/ │ └── Format.php ├── Model/ │ ├── Config.php │ ├── Driver/ │ │ └── Output/ │ │ └── Html.php │ └── Storage.php ├── Profile/ │ ├── Code.php │ ├── Context.php │ ├── Database.php │ ├── General.php │ ├── Meta.php │ └── Pool.php ├── etc/ │ ├── di.xml │ ├── frontend/ │ │ └── routes.xml │ └── module.xml └── view/ └── base/ ├── layout/ │ ├── profiler_profile_index.xml │ └── profiler_profile_view.xml ├── page_layout/ │ └── profiler.xml ├── templates/ │ ├── profile/ │ │ ├── listing.phtml │ │ └── view.phtml │ ├── root.phtml │ ├── tab/ │ │ ├── code.phtml │ │ ├── database.phtml │ │ └── io.phtml │ └── toolbar.phtml └── web/ ├── css/ │ ├── less/ │ │ ├── _module/ │ │ │ ├── _base.less │ │ │ ├── _etc.less │ │ │ └── _tabs.less │ │ └── _module.less │ └── module.css └── js/ ├── lib/ │ ├── jquery.filtertable.js │ ├── jquery.tablesorter.js │ └── jquery.treetable.js └── table.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE.txt ================================================ Open Software License ("OSL") v. 3.0 This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: Licensed under the Open Software License version 3.0 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: 1. to reproduce the Original Work in copies, either alone or as part of a collective work; 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; 4. to perform the Original Work publicly; and 5. to display the Original Work publicly. 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. ================================================ FILE: README.md ================================================ # Magento 2 Profiler Module # ## Installation Log in to the Magento server, go to your Magento install dir and run these commands: ``` composer require mirasvit/module-profiler php -f bin/magento module:enable Mirasvit_Profiler php -f bin/magento setup:upgrade php -f bin/magento mirasvit:profiler:enable ``` ## Usage ``` php -f bin/magento mirasvit:profiler:enable # Enable profiler php -f bin/magento mirasvit:profiler:disable # Disable profiler php -f bin/magento mirasvit:profiler:status # Current status php -f bin/magento mirasvit:profiler:allow-ips 127.0.0.1 192.268.22.11 # Allow only specified IPs php -f bin/magento mirasvit:profiler:allow-ips --none # Remove IP restriction ``` ## Demo [http://profiler.m2.mirasvit.com/](http://profiler.m2.mirasvit.com/) [http://profiler.m2.mirasvit.com/profiler/profile/index/](http://profiler.m2.mirasvit.com/profiler/profile/index/) ## Screenshots ### Magento 2 Code Profiler and Database Profiler ![](http://mirasvit.com/media/profiler/v2.png) ## Licence [Open Software License (OSL 3.0)](http://opensource.org/licenses/osl-3.0.php) ## 1.0.8 *(2020-10-08) #### Improvements * M2.4 --- ## 1.0.6 *(2017-09-28)* #### Improvements * M2.2 #### Fixed * Issue #20 --- ## 1.0.5 *(2017-09-07)* * PHP 5.6.x --- ## 1.0.3, 1.0.4 *(2017-09-06)* * Issues with less compilation --- ## 1.0.2 *(2017-09-05)* * Significant changes in UI --- ## 1.0.1 *(2017-03-30)* * Improve styles load mechanism --- ## 1.0.0 *(2017-03-30)* * Initial release ================================================ FILE: composer.json ================================================ { "name": "mirasvit/module-profiler", "description": "Magento 2 Profiler", "require": { "magento/framework": "100.*|101.*|102.*|103.*", "jdorn/sql-formatter": "^1.2", "symfony/yaml": ">=2.2.4" }, "version": "1.0.8", "type": "magento2-module", "license": [ "proprietary" ], "autoload": { "files": [ "./registration.php" ], "psr-4": { "Mirasvit\\Profiler\\": "src/Profiler" } } } ================================================ FILE: registration.php ================================================ [ [ 'output' => 'Mirasvit\Profiler\Model\Driver\Output\Html', 'stat' => $_SERVER['MAGE_PROFILER_STAT'], ], ], ], 'BP', false); } \Magento\Framework\Component\ComponentRegistrar::register( \Magento\Framework\Component\ComponentRegistrar::MODULE, 'Mirasvit_Profiler', __DIR__ . '/src/Profiler' ); ================================================ FILE: src/Profiler/Api/Data/ProfileInterface.php ================================================ request = $request; $this->storage = $storage; } public function getProfile($id = false) { if (!$id) { $id = $this->request->getParam('id'); } return $this->storage->load($id); } // /** // * @param \Magento\Framework\Profiler\Driver\Standard\Stat $stat // * @return $this // */ // public function setProfilerStat($stat) // { // $this->profilerStat = $stat; // // return $this; // } // // /** // * @return \Magento\Framework\Profiler\Driver\Standard\Stat // */ // public function getProfilerStat() // { // return $this->profilerStat; // } // // /** // * @return \Zend_Db_Profiler // */ // public function getDbProfiler() // { // return $this->resourceConnection->getConnection('read') // ->getProfiler(); // } } ================================================ FILE: src/Profiler/Block/Profile/Listing.php ================================================ storage = $storage; parent::__construct($context, $data); } public function getList() { return $this->storage->getList(); } } ================================================ FILE: src/Profiler/Block/Profile/View.php ================================================ request = $context->getRequest(); $this->storage = $storage; $this->tabs = $tabs; parent::__construct($context); } public function getProfile() { return $this->storage->load($this->getRequest()->getParam('id')); } /** * @return TabInterface[] */ public function getTabs() { return $this->tabs; } } ================================================ FILE: src/Profiler/Block/Tab/Code.php ================================================ context = $context; parent::__construct($templateContext, $data); } /** * {@inheritdoc} */ public function getLabel() { return 'Performance'; } /** * {@inheritdoc} */ public function getIcon() { return 'clock-o'; } /** * @return array */ public function getCodeDump() { return $this->context->getProfile()['code']; } public function getFlameGraphJson() { $frameGraph = [ 'name' => 'root', 'value' => $this->getGeneralDump()[\Mirasvit\Profiler\Profile\General::EXECUTION_TIME] / 1000, 'children' => [], ]; foreach ($this->context->getProfile()['code'] as $path => $data) { $data['value'] = $data['sum']; $data['children'] = []; $selectedNode = &$frameGraph; foreach(explode('->', $path) as $node) { $children = &$selectedNode['children']; if (! isset($children[$node])) { $data['name'] = $node; $children[$node] = $data; } $selectedNode = &$children[$node]; } } return \json_encode($this->_removeChildrenKeys($frameGraph)); } private function _removeChildrenKeys($node) { $node['children'] = array_values($node['children']); foreach ($node['children'] as &$child) { $child = $this->_removeChildrenKeys($child); } return $node; } /** * @return array */ public function getGeneralDump() { return $this->context->getProfile()['general']; } public function getLevel($timerId) { return substr_count($timerId, '->'); } /** * @param int $timerId * @return string */ public function renderTimerId($timerId) { $nestingSep = preg_quote('->', '/'); return preg_replace('/.+?' . $nestingSep . '/', '', $timerId); } /** * @param int $timerId * @return string */ public function getParentTimerId($timerId) { $timerId = explode('->', $timerId); array_pop($timerId); return implode('->', $timerId); } /** * @param int $timerId * @return float */ public function getTimerLength($timerId) { return 0; $total = microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']; return round($this->getStat()->fetch($timerId, 'sum') / $total * 100, 2); } /** * @return float */ public function getTotalTime() { return microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']; } } ================================================ FILE: src/Profiler/Block/Tab/Database.php ================================================ context = $context; parent::__construct($templateContext); } /** * {@inheritdoc} */ public function getLabel() { return __('Database'); } /** * {@inheritdoc} */ public function getIcon() { return 'database'; } /** * @return array */ public function getDump() { return $this->context->getProfile()['database']; } /** * @return array */ public function getSlowQueries() { $queries = []; /** @var \Zend_Db_Profiler_Query $query */ foreach ($this->getDbProfiler()->getQueryProfiles() as $queryId => $query) { $queries[$queryId] = $query->getElapsedSecs(); } arsort($queries); $queries = array_slice($queries, 0, 5, true); return $queries; } } ================================================ FILE: src/Profiler/Block/Tab/IO.php ================================================ context = $context; parent::__construct($templateContext, $data); } /** * {@inheritdoc} */ public function getLabel() { return 'Request / Response'; } /** * {@inheritdoc} */ public function getIcon() { return 'globe'; } /** * @return array */ public function getDump() { return $this->context->getProfile(); } } ================================================ FILE: src/Profiler/Block/Tab/TabInterface.php ================================================ context = $context; $this->urlHelper = $urlHelper; parent::__construct($templateContext); } /** * @return array */ public function getDump() { return $this->context->getProfile($this->getProfileId()); } /** * @return string */ public function getProfileUrl() { return $this->urlHelper->getUrl('profiler/profile/view', ['id' => $this->getProfileId()]); } } ================================================ FILE: src/Profiler/Console/Command/AbstractCommand.php ================================================ appState = $appState; parent::__construct(); } } ================================================ FILE: src/Profiler/Console/Command/AllowIpsCommand.php ================================================ config = $config; parent::__construct($appState); } /** * {@inheritdoc} */ protected function configure() { $arguments = [ new InputArgument( 'ip', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Allowed IP addresses' ), ]; $options = [ new InputOption( 'none', null, InputOption::VALUE_NONE, 'Clear allowed IP addresses' ), ]; $this->setName('mirasvit:profiler:allow-ips') ->setDescription('Enable profiler only for specified IPs') ->setDefinition(array_merge($arguments, $options)); parent::configure(); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { try{ $this->appState->setAreaCode('empty'); }catch (LocalizedException $e){} if (!$input->getOption('none')) { $addresses = $input->getArgument('ip'); if (!empty($addresses)) { $this->config->setAddresses(implode(',', $addresses)); $output->writeln( 'Set exempt IP-addresses: ' . implode(', ', $this->config->getAddressInfo()) . '' ); } } else { $this->config->setAddresses(''); $output->writeln('Set exempt IP-addresses: none'); } } } ================================================ FILE: src/Profiler/Console/Command/DisableCommand.php ================================================ config = $config; parent::__construct($appState); } /** * {@inheritdoc} */ protected function configure() { $this->setName('mirasvit:profiler:disable') ->setDescription('Disable profiler') ->setDefinition([]); parent::configure(); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { try { $this->appState->setAreaCode('empty'); } catch (LocalizedException $e) { } $this->config->disableProfiler(); $output->writeln('Status: ' . ($this->config->isEnabled() ? 'Enabled' : 'Disabled') . ''); } } ================================================ FILE: src/Profiler/Console/Command/EnableCommand.php ================================================ config = $config; parent::__construct($appState); } /** * {@inheritdoc} */ protected function configure() { $this->setName('mirasvit:profiler:enable') ->setDescription('Enable profiler') ->setDefinition([]); parent::configure(); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { try { $this->appState->setAreaCode('empty'); } catch (LocalizedException $e) { } $this->config->enableProfiler(); $output->writeln('Status: ' . ($this->config->isEnabled() ? 'Enabled' : 'Disabled') . ''); } } ================================================ FILE: src/Profiler/Console/Command/StatusCommand.php ================================================ config = $config; parent::__construct($appState); } /** * {@inheritdoc} */ protected function configure() { $this->setName('mirasvit:profiler:status') ->setDescription('Profiler status') ->setDefinition([]); parent::configure(); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { try{ $this->appState->setAreaCode('empty'); }catch (LocalizedException $e){} $output->writeln('Status: ' . ($this->config->isEnabled() ? 'Enabled' : 'Disabled') . ''); $output->writeln('IPs: ' . implode(', ', $this->config->getAddressInfo()) . ''); } } ================================================ FILE: src/Profiler/Controller/Profile/Index.php ================================================ resultFactory->create(ResultFactory::TYPE_PAGE, [ 'template' => 'Mirasvit_Profiler::root.phtml', ]); $resultPage->getConfig()->getTitle()->set(__('Profiler')); return $resultPage; } } ================================================ FILE: src/Profiler/Controller/Profile/View.php ================================================ resultFactory->create(ResultFactory::TYPE_PAGE, [ 'template' => 'Mirasvit_Profiler::root.phtml', ]); $resultPage->getConfig()->getTitle()->set(__('Profiler')); return $resultPage; } } ================================================ FILE: src/Profiler/Controller/Profile.php ================================================ ' . print_r($any, true) . ''; } } else { return json_encode($any); } return ''; } } ================================================ FILE: src/Profiler/Model/Config.php ================================================ deploymentConfigWriter = $deploymentConfigWriter; $this->deploymentConfigReader = $deploymentConfigReader; $this->scopeConfig = $scopeConfig; $this->configFactory = $configFactory; $this->directoryList = $directoryList; } /** * @return bool */ public function isEnabled() { return (bool)$this->scopeConfig->getValue('profiler/general/enable'); } /** * @return bool */ public function enableProfiler() { $config = $this->configFactory->create(); $config->setDataByPath('profiler/general/enable', true); $config->save(); $this->enableDbProfiler(); return true; } /** * @return bool */ public function disableProfiler() { $config = $this->configFactory->create(); $config->setDataByPath('profiler/general/enable', false); $config->save(); $this->disableDbProfiler(); return true; } /** * @return bool * @throws \Exception */ public function enableDbProfiler() { $env = $this->deploymentConfigReader->load(ConfigFilePool::APP_ENV); $env['db']['connection']['default']['profiler'] = [ 'class' => '\\Magento\\Framework\\DB\\Profiler', 'enabled' => true, ]; $this->deploymentConfigWriter->saveConfig([ConfigFilePool::APP_ENV => $env], true); return true; } /** * @return bool * @throws \Exception */ public function disableDbProfiler() { $env = $this->deploymentConfigReader->load(ConfigFilePool::APP_ENV); unset($env['db']['connection']['default']['profiler']); $this->deploymentConfigWriter->saveConfig([ConfigFilePool::APP_ENV => $env], true); return true; } /** * @param string $addresses * @return bool */ public function setAddresses($addresses) { $config = $this->configFactory->create(); $config->setDataByPath('profiler/general/addresses', $addresses); $config->save(); return true; } /** * @return array */ public function getAddressInfo() { $addresses = $this->scopeConfig->getValue('profiler/general/addresses'); return array_filter(explode(',', $addresses)); } public function getDumpPath() { $path = $this->directoryList->getPath('var').'/profiler/'; if (!file_exists($path)) { mkdir($path); } return $path; } } ================================================ FILE: src/Profiler/Model/Driver/Output/Html.php ================================================ get('Mirasvit\Profiler\Model\Config'); if (!$config->isEnabled()) { return; } $addresses = $config->getAddressInfo(); if (count($addresses) && isset($_SERVER['REMOTE_ADDR']) && !in_array($_SERVER['REMOTE_ADDR'], $addresses)) { return; } if (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], 'profiler') !== false) { return; } /** @var \Mirasvit\Profiler\Model\Storage $storage */ $storage = $objectManager->get('Mirasvit\Profiler\Model\Storage'); $profileId = $storage->dump(); $isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'; if (!$isAjax && PHP_SAPI != 'cli' && strpos($_SERVER['HTTP_ACCEPT'], 'text/html') !== false) { /** @var \Magento\Framework\View\LayoutInterface $layout */ $layout = $objectManager->create('Magento\Framework\View\LayoutInterface'); echo $layout->createBlock('Mirasvit\Profiler\Block\Toolbar') ->setProfileId($profileId) ->toHtml(); } } } ================================================ FILE: src/Profiler/Model/Storage.php ================================================ pool = $profilePool; $this->config = $config; } /** * @return string */ public function dump() { $dump = []; foreach ($this->pool->getProfiles() as $code => $profile) { $dump[$code] = $profile->dump(); } if (!isset($dump['meta'])) { return false; } $meta = $dump['meta']; $dt = (\DateTime::createFromFormat('U.u', microtime(true))); $name = $dt->format('Y-m-d H:i:s.u'); $file = $this->config->getDumpPath() . $name . '.meta'; file_put_contents($file, (new YamlDumper())->dump($meta, 10)); $file = $this->config->getDumpPath() . $name . '.prof'; file_put_contents($file, (new YamlDumper())->dump($dump, 10)); $this->cleanup(); return $name; } /** * @return array */ public function load($file) { $content = file_get_contents($this->config->getDumpPath() . '/' . $file . '.prof'); $dump = (new YamlParser())->parse($content); return $dump; } public function getList() { $result = []; $files = scandir($this->config->getDumpPath(), 1); foreach ($files as $file) { if ($file[0] == '.') { continue; } if (pathinfo($file)['extension'] != 'meta') { continue; } $content = file_get_contents($this->config->getDumpPath() . '/' . $file); $meta = (new YamlParser())->parse($content); $meta['ID'] = pathinfo($file)['filename']; $result[] = $meta; } return $result; } public function cleanup() { $limit = 100; $files = scandir($this->config->getDumpPath(), 1); foreach ($files as $file) { if ($file[0] == '.') { continue; } if (pathinfo($file)['extension'] != 'meta') { continue; } $limit--; if ($limit < 0) { $name = pathinfo($file)['filename']; @unlink($this->config->getDumpPath() . '/' . $name . '.meta'); @unlink($this->config->getDumpPath() . '/' . $name . '.prof'); } } } } ================================================ FILE: src/Profiler/Profile/Code.php ================================================ getStat()->getFilteredTimerIds() as $timerId) { $dump[$timerId] = $this->getStat()->get($timerId); } return $dump; } /** * @return \Magento\Framework\Profiler\Driver\Standard\Stat */ private function getStat() { return $_SERVER['MAGE_PROFILER_STAT']; } } ================================================ FILE: src/Profiler/Profile/Context.php ================================================ isCLI()) { global $argv; return implode(' ', $argv); } return false; } } ================================================ FILE: src/Profiler/Profile/Database.php ================================================ resourceConnection = $resourceConnection; } /** * {@inheritdoc} */ public function dump() { $queryCountBucket = []; $dump = [ self::TOTAL_ELAPSED => $this->getProfiler()->getTotalElapsedSecs() * 1000, self::TOTAL_QUERIES => $this->getProfiler()->getTotalNumQueries(), 'profiles' => [], ]; $profiles = $this->getProfiler()->getQueryProfiles(); if (is_array($profiles)) { /** @var \Zend_Db_Profiler_Query $profile */ foreach ($profiles as $profile) { if (!isset($queryCountBucket[$profile->getQuery()])) { $queryCountBucket[$profile->getQuery()] = 0; } $queryCountBucket[$profile->getQuery()]++; $dump['profiles'][] = [ self::QUERY => $profile->getQuery(), self::QUERY_TYPE => $profile->getQueryType(), self::QUERY_PARAMS => $profile->getQueryParams(), self::QUERY_ELAPSED => $profile->getElapsedSecs() * 1000, self::QUERY_STARTED => $profile->getStartedMicrotime(), self::QUERY_COUNT => $queryCountBucket[$profile->getQuery()] ]; } } return $dump; } /** * @return \Zend_Db_Profiler */ private function getProfiler() { return $this->resourceConnection->getConnection('read') ->getProfiler(); } } ================================================ FILE: src/Profiler/Profile/General.php ================================================ resourceConnection = $resourceConnection; $this->context = $context; } /** * {@inheritdoc} */ public function dump() { /** @var \Zend_Db_Profiler $db */ $db = $this->resourceConnection->getConnection('read') ->getProfiler(); $dump = [ self::EXECUTION_TIME => $this->context->getExecutionTime(), self::IP => $this->context->getClientIP(), self::URI => $this->context->getURI(), self::IS_CLI => $this->context->isCLI(), self::CLI_ARGS => $this->context->getCliArgs(), self::DB_QUERIES => $db->getTotalNumQueries(), self::DB_TIME => round($db->getTotalElapsedSecs() * 1000), self::GET => isset($_GET) ? $_GET : [], self::POST => isset($_POST) ? $_POST : [], ]; return $dump; } } ================================================ FILE: src/Profiler/Profile/Meta.php ================================================ context = $context; } /** * {@inheritdoc} */ public function dump() { if ($this->context->isCLI()) { $url = $this->context->getCliArgs(); } else { $url = $this->context->getURI(); } $dump = [ 'RESPONSE_CODE' => http_response_code(), 'METHOD' => $this->context->isCLI() ? 'CLI' : $_SERVER['REQUEST_METHOD'], 'TIME' => microtime(true), 'EXECUTION_TIME' => $this->context->getExecutionTime(), 'URL' => $url, 'IP' => $this->context->getClientIP(), ]; return $dump; } } ================================================ FILE: src/Profiler/Profile/Pool.php ================================================ profiles = $profiles; } /** * @return ProfileInterface[] */ public function getProfiles() { return $this->profiles; } } ================================================ FILE: src/Profiler/etc/di.xml ================================================ Mirasvit\Profiler\Console\Command\EnableCommand Mirasvit\Profiler\Console\Command\DisableCommand Mirasvit\Profiler\Console\Command\AllowIpsCommand Mirasvit\Profiler\Console\Command\StatusCommand Mirasvit\Profiler\Block\Tab\IO Mirasvit\Profiler\Block\Tab\Code Mirasvit\Profiler\Block\Tab\Database Mirasvit\Profiler\Profile\General Mirasvit\Profiler\Profile\Meta Mirasvit\Profiler\Profile\Code Mirasvit\Profiler\Profile\Database ================================================ FILE: src/Profiler/etc/frontend/routes.xml ================================================ ================================================ FILE: src/Profiler/etc/module.xml ================================================ ================================================ FILE: src/Profiler/view/base/layout/profiler_profile_index.xml ================================================ ================================================ FILE: src/Profiler/view/base/layout/profiler_profile_view.xml ================================================ ================================================ FILE: src/Profiler/view/base/page_layout/profiler.xml ================================================ ================================================ FILE: src/Profiler/view/base/templates/profile/listing.phtml ================================================ helper('Mirasvit\Profiler\Helper\Format'); ?> getList() as $item): ?>
Status Method IP Time Execution ms URL
view
formatTime($item[Meta::EXECUTION_TIME]) ?>
================================================ FILE: src/Profiler/view/base/templates/profile/view.phtml ================================================ getProfile(); $general = $profile['general']; ?>
getTabs() as $key => $tab): ?>
toHtml() ?>
================================================ FILE: src/Profiler/view/base/templates/root.phtml ================================================ ================================================ FILE: src/Profiler/view/base/templates/tab/code.phtml ================================================ getGeneralDump(); /** @var \Mirasvit\Profiler\Helper\Format $format */ $format = $this->helper(\Mirasvit\Profiler\Helper\Format::class); ?>
formatTime($general[General::EXECUTION_TIME]) ?> ms Total Time
formatTime($general[General::DB_TIME]) ?> ms Database Time
================================================ FILE: src/Profiler/view/base/templates/tab/database.phtml ================================================ getDump(); /** @var \Mirasvit\Profiler\Helper\Format $format */ $format = $this->helper('Mirasvit\Profiler\Helper\Format'); ?>
formatTime($dump[Database::TOTAL_ELAPSED]) ?> ms Total Queries Time
Total Queries
# Query Occurance Time ms SQL Query Query Params
formatTime($query[Database::QUERY_ELAPSED]) ?> any($query[Database::QUERY_PARAMS]) ?>
================================================ FILE: src/Profiler/view/base/templates/tab/io.phtml ================================================ getDump(); $general = $dump['general']; /** @var \Mirasvit\Profiler\Helper\Format $format */ $format = $this->helper('Mirasvit\Profiler\Helper\Format'); ?>
URL
GET Params any($general[General::GET]) ?>
POST Params any($general[General::POST]) ?>
IP
================================================ FILE: src/Profiler/view/base/templates/toolbar.phtml ================================================ getDump(); $meta = $dump['meta']; $database = $dump['database']; ?>
ms
in ms
View
================================================ FILE: src/Profiler/view/base/web/css/less/_module/_base.less ================================================ html, body { margin: 0; font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 13px; color: @text-color; } a { color: @link-color; &:hover { text-decoration: none; } } pre { background: transparent !important; margin: 0; } table { border-collapse: collapse; border-spacing: 0; width: 100%; border: none; tr { th, td { padding: 10px; } th { background-color: @bg-dark; border: 1px solid @border-dark; color: #ffffff; font-weight: 600; white-space: nowrap; &:first-child { border-left: 0; } &:last-child { border-right: 0; } } td { background-color: #ffffff; border-left: 1px dashed @border-light; border-right: 1px dashed @border-light; vertical-align: top; &:first-child { border-left: 0; } &:last-child { border-right: 0; } } &:nth-child(even) { td { background: @bg-light; } } &:last-child { td { border-bottom: 1px solid @border-light; } } &:hover { td { background: #e5f7fe; } } } } .tablesorter { .tablesorter-header { text-decoration: underline; &.tablesorter-headerDesc, &.tablesorter-headerAsc { background: lighten(@bg-dark, 20%); } sup { text-decoration: none; } } } .text-left { text-align: left; } .text-right { text-align: right; } .text-center { text-align: center; } ================================================ FILE: src/Profiler/view/base/web/css/less/_module/_etc.less ================================================ .metric { border: 1px solid @border-light; display: inline-block; margin: 10px 10px 10px 0; strong { text-align: center; font-size: 25px; font-weight: 300; display: block; padding: 10px; } i { display: block; background: @border-light; font-style: normal; text-align: center; padding: 3px 10px; font-size: 12px; font-weight: 600; } } .badge { border-radius: 1px; background: #eee; &.code-200 { background: #00c602; color: #fff; } &.code-301, &.code-302 { background: #eebe00; color: #fff; } &.code-404, &.code-503, &.code-500 { background: #c6000c; color: #fff; } } ================================================ FILE: src/Profiler/view/base/web/css/less/_module/_tabs.less ================================================ .tabs-container { .tabs { list-style: none; padding: 0 10px 0 10px; margin: 0; background: @bg-dark; li { display: inline-block; background: @bg-dark; color: #fff; font-weight: 600; padding: 10px; cursor: pointer; border-bottom: 3px solid transparent; a { color: #fff; } &:hover { border-bottom: 3px solid @orange; } &._active { background: @bg-dark-hover; border-bottom: 3px solid @orange; } &.link { border-bottom: 3px solid transparent !important; } } } .tab-content { width: 100%; display: none; padding: 10px; box-sizing: border-box; } } ================================================ FILE: src/Profiler/view/base/web/css/less/_module.less ================================================ @import url('//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css'); @text-color: #303030; @link-color: #278CBA; @bg-light: #f5f5f5; @bg-dark: #514943; @bg-dark-hover: lighten(#514943, 10%); @border-dark: #8a837f; @border-light: #d6d6d6; @orange: #EE5100; @import "_module/_base.less"; @import "_module/_tabs.less"; @import "_module/_etc.less"; ================================================ FILE: src/Profiler/view/base/web/css/module.css ================================================ @import url('//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css'); html, body { margin: 0; font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 13px; color: #303030; } a { color: #278cba; } a:hover { text-decoration: none; } pre { background: transparent !important; margin: 0; } table { border-collapse: collapse; border-spacing: 0; width: 100%; border: none; } table tr th, table tr td { padding: 10px; } table tr th { background-color: #514943; border: 1px solid #8a837f; color: #ffffff; font-weight: 600; white-space: nowrap; } table tr th:first-child { border-left: 0; } table tr th:last-child { border-right: 0; } table tr td { background-color: #ffffff; border-left: 1px dashed #d6d6d6; border-right: 1px dashed #d6d6d6; vertical-align: top; } table tr td:first-child { border-left: 0; } table tr td:last-child { border-right: 0; } table tr:nth-child(even) td { background: #f5f5f5; } table tr:last-child td { border-bottom: 1px solid #d6d6d6; } table tr:hover td { background: #e5f7fe; } .tablesorter .tablesorter-header { text-decoration: underline; } .tablesorter .tablesorter-header.tablesorter-headerDesc, .tablesorter .tablesorter-header.tablesorter-headerAsc { background: #897b71; } .tablesorter .tablesorter-header sup { text-decoration: none; } .text-left { text-align: left; } .text-right { text-align: right; } .text-center { text-align: center; } .tabs-container .tabs { list-style: none; padding: 0 10px 0 10px; margin: 0; background: #514943; } .tabs-container .tabs li { display: inline-block; background: #514943; color: #fff; font-weight: 600; padding: 10px; cursor: pointer; border-bottom: 3px solid transparent; } .tabs-container .tabs li a { color: #fff; } .tabs-container .tabs li:hover { border-bottom: 3px solid #ee5100; } .tabs-container .tabs li._active { background: #6d625a; border-bottom: 3px solid #ee5100; } .tabs-container .tabs li.link { border-bottom: 3px solid transparent !important; } .tabs-container .tab-content { width: 100%; display: none; padding: 10px; box-sizing: border-box; } .metric { border: 1px solid #d6d6d6; display: inline-block; margin: 10px 10px 10px 0; } .metric strong { text-align: center; font-size: 25px; font-weight: 300; display: block; padding: 10px; } .metric i { display: block; background: #d6d6d6; font-style: normal; text-align: center; padding: 3px 10px; font-size: 12px; font-weight: 600; } .badge { border-radius: 1px; background: #eee; } .badge.code-200 { background: #00c602; color: #fff; } .badge.code-301, .badge.code-302 { background: #eebe00; color: #fff; } .badge.code-404, .badge.code-503, .badge.code-500 { background: #c6000c; color: #fff; } ================================================ FILE: src/Profiler/view/base/web/js/lib/jquery.filtertable.js ================================================ /** * jquery.filterTable * * This plugin will add a search filter to tables. When typing in the filter, * any rows that do not contain the filter will be hidden. * * Utilizes bindWithDelay() if available. https://github.com/bgrins/bindWithDelay * * @version v1.5.5 * @author Sunny Walker, swalker@hawaii.edu * @license MIT */ (function ($) { var jversion = $.fn.jquery.split('.'), jmajor = parseFloat(jversion[0]), jminor = parseFloat(jversion[1]); if (jmajor < 2 && jminor < 8) { // build the pseudo selector for jQuery < 1.8 $.expr[':'].filterTableFind = function (a, i, m) { // build the case insensitive filtering functionality as a pseudo-selector expression return $(a).text().toUpperCase().indexOf(m[3].toUpperCase()) >= 0; }; $.expr[':'].filterTableFindAny = function (a, i, m) { // build the case insensitive all-words filtering functionality as a pseudo-selector expression // build an array of each non-falsey value passed var raw_args = m[3].split(/[\s,]/), args = []; $.each(raw_args, function (j, v) { var t = v.replace(/^\s+|\s$/g, ''); if (t) { args.push(t); } }); // if there aren't any non-falsey values to search for, abort if (!args.length) { return false; } return function (a) { var found = false; $.each(args, function (j, v) { if ($(a).text().toUpperCase().indexOf(v.toUpperCase()) >= 0) { found = true; return false; } }); return found; }; }; $.expr[':'].filterTableFindAll = function (a, i, m) { // build the case insensitive all-words filtering functionality as a pseudo-selector expression // build an array of each non-falsey value passed var raw_args = m[3].split(/[\s,]/), args = []; $.each(raw_args, function (j, v) { var t = v.replace(/^\s+|\s$/g, ''); if (t) { args.push(t); } }); // if there aren't any non-falsey values to search for, abort if (!args.length) { return false; } return function (a) { var found = 0; // how many terms were found? $.each(args, function (j, v) { if ($(a).text().toUpperCase().indexOf(v.toUpperCase()) >= 0) { found++; // found another term } }); return found === args.length; // did we find all of them in this cell? }; }; } else { // build the pseudo selector for jQuery >= 1.8 $.expr[':'].filterTableFind = jQuery.expr.createPseudo(function (arg) { return function (el) { return $(el).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; }; }); $.expr[':'].filterTableFindAny = jQuery.expr.createPseudo(function (arg) { // build an array of each non-falsey value passed var raw_args = arg.split(/[\s,]/), args = []; $.each(raw_args, function (i, v) { var t = v.replace(/^\s+|\s$/g, ''); // trim the string if (t) { args.push(t); } }); // if there aren't any non-falsey values to search for, abort if (!args.length) { return false; } return function (el) { var found = false; $.each(args, function (i, v) { if ($(el).text().toUpperCase().indexOf(v.toUpperCase()) >= 0) { found = true; return false; // short-circuit the searching since this cell has one of the terms } }); return found; }; }); $.expr[':'].filterTableFindAll = jQuery.expr.createPseudo(function (arg) { // build an array of each non-falsey value passed var raw_args = arg.split(/[\s,]/), args = []; $.each(raw_args, function (i, v) { var t = v.replace(/^\s+|\s$/g, ''); // trim the string if (t) { args.push(t); } }); // if there aren't any non-falsey values to search for, abort if (!args.length) { return false; } return function (el) { var found = 0; // how many terms were found? $.each(args, function (i, v) { if ($(el).text().toUpperCase().indexOf(v.toUpperCase()) >= 0) { found++; // found another term } }); return found === args.length; // did we find all of them in this cell? }; }); } $.fn.filterTable = function (options) { // define the filterTable plugin var defaults = { // start off with some default settings autofocus: false, // make the filter input field autofocused (not recommended for accessibility) callback: null, // callback function: function(term, table){} containerClass: 'filter-table', // class to apply to the container containerTag: 'p', // tag name of the container filterExpression: 'filterTableFind', // jQuery expression method to use for filtering hideTFootOnFilter: false, // if true, the table's tfoot(s) will be hidden when the table is filtered highlightClass: 'alt', // class applied to cells containing the filter term ignoreClass: '', // don't filter the contents of cells with this class ignoreColumns: [], // don't filter the contents of these columns inputSelector: null, // use the element with this selector for the filter input field instead of creating one inputName: '', // name of filter input field inputType: 'search', // tag name of the filter input tag label: '', // text to precede the filter input tag minChars: 1, // filter only when at least this number of characters are in the filter input field minRows: 8, // don't show the filter on tables with at least this number of rows placeholder: 'search this table', // HTML5 placeholder text for the filter field preventReturnKey: true, // prevent the return key in the filter input field from trigger form submits quickList: [], // list of phrases to quick fill the search quickListClass: 'quick', // class of each quick list item quickListGroupTag: '', // tag surrounding quick list items (e.g., ul) quickListTag: 'a', // tag type of each quick list item (e.g., a or li) visibleClass: 'visible' // class applied to visible rows }, hsc = function (text) { // mimic PHP's htmlspecialchars() function return text.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); }, settings = $.extend({}, defaults, options); // merge the user's settings into the defaults var doFiltering = function (table, q) { // handle the actual table filtering var tbody = table.find('tbody'); // cache the tbody element if (q === '' || q.length < settings.minChars) { // if the filtering query is blank or the number of chars is less than the minChars option tbody.find('tr').show().addClass(settings.visibleClass); // show all rows tbody.find('td').removeClass(settings.highlightClass); // remove the row highlight from all cells if (settings.hideTFootOnFilter) { // show footer if the setting was specified table.find('tfoot').show(); } } else { // if the filter query is not blank var all_tds = tbody.find('td'); tbody.find('tr').hide().removeClass(settings.visibleClass); // hide all rows, assuming none were found all_tds.removeClass(settings.highlightClass); // remove previous highlights if (settings.hideTFootOnFilter) { // hide footer if the setting was specified table.find('tfoot').hide(); } if (settings.ignoreColumns.length) { var tds = []; if (settings.ignoreClass) { all_tds = all_tds.not('.' + settings.ignoreClass); } tds = all_tds.filter(':' + settings.filterExpression + '("' + q.replace(/(['"])/g, '\\$1') + '")'); tds.each(function () { var t = $(this), col = t.parent().children().index(t); //window.console.log(t.text(), col); if ($.inArray(col, settings.ignoreColumns) === -1) { //window.console.log(t.text(), $.inArray(col, settings.ignoreColumns)); t.addClass(settings.highlightClass).closest('tr').show().addClass(settings.visibleClass); } }); } else { if (settings.ignoreClass) { all_tds = all_tds.not('.' + settings.ignoreClass); } all_tds.filter(':' + settings.filterExpression + '("' + q.replace(/(['"])/g, '\\$1') + '")').addClass(settings.highlightClass).closest('tr').show().addClass(settings.visibleClass); // highlight (class=alt) only the cells that match the query and show their rows } } if (settings.callback) { // call the callback function settings.callback(q, table); } }; // doFiltering() return this.each(function () { var t = $(this), // cache the table tbody = t.find('tbody'), // cache the tbody container = null, // placeholder for the filter field container DOM node quicks = null, // placeholder for the quick list items filter = null, // placeholder for the field field DOM node created_filter = true; // was the filter created or chosen from an existing element? if (t[0].nodeName === 'TABLE' && tbody.length > 0 && (settings.minRows === 0 || (settings.minRows > 0 && tbody.find('tr').length >= settings.minRows)) && !t.prev().hasClass(settings.containerClass)) { // only if object is a table and there's a tbody and at least minRows trs and hasn't already had a filter added if (settings.inputSelector && $(settings.inputSelector).length === 1) { // use a single existing field as the filter input field filter = $(settings.inputSelector); container = filter.parent(); // container to hold the quick list options created_filter = false; } else { // create the filter input field (and container) container = $('<' + settings.containerTag + ' />'); // build the container tag for the filter field if (settings.containerClass !== '') { // add any classes that need to be added container.addClass(settings.containerClass); } container.prepend(settings.label + ' '); // add the label for the filter field filter = $(''); // build the filter field if (settings.preventReturnKey) { // prevent return in the filter field from submitting any forms filter.on('keydown', function (ev) { if ((ev.keyCode || ev.which) === 13) { ev.preventDefault(); return false; } }); } } if (settings.autofocus) { // add the autofocus attribute if requested filter.attr('autofocus', true); } if ($.fn.bindWithDelay) { // does bindWithDelay() exist? filter.bindWithDelay('keyup', function () { // bind doFiltering() to keyup (delayed) doFiltering(t, $(this).val()); }, 200); } else { // just bind to onKeyUp filter.bind('keyup', function () { // bind doFiltering() to keyup doFiltering(t, $(this).val()); }); } // keyup binding block filter.bind('click search input paste blur', function () { // bind doFiltering() to additional events doFiltering(t, $(this).val()); }); if (created_filter) { // add the filter field to the container if it was created by the plugin container.append(filter); } if (settings.quickList.length > 0) { // are there any quick list items to add? quicks = settings.quickListGroupTag ? $('<' + settings.quickListGroupTag + ' />') : container; $.each(settings.quickList, function (index, value) { // for each quick list item... var q = $('<' + settings.quickListTag + ' class="' + settings.quickListClass + '" />'); // build the quick list item link q.text(hsc(value)); // add the item's text if (q[0].nodeName === 'A') { q.attr('href', '#'); // add a (worthless) href to the item if it's an anchor tag so that it gets the browser's link treatment } q.bind('click', function (e) { // bind the click event to it e.preventDefault(); // stop the normal anchor tag behavior from happening filter.val(value).focus().trigger('click'); // send the quick list value over to the filter field and trigger the event }); quicks.append(q); // add the quick list link to the quick list groups container }); // each quick list item if (quicks !== container) { container.append(quicks); // add the quick list groups container to the DOM if it isn't already there } } // if quick list items if (created_filter) { // add the filter field and quick list container to just before the table if it was created by the plugin t.before(container); } } // if the functionality should be added }); // return this.each }; // $.fn.filterTable })(jQuery); ================================================ FILE: src/Profiler/view/base/web/js/lib/jquery.tablesorter.js ================================================ /* * * TableSorter 2.0 - Client-side table sorting with ease! * Version 2.0.5b * @requires jQuery v1.2.3 * * Copyright (c) 2007 Christian Bach * Examples and docs at: http://tablesorter.com * Dual licensed under the MIT and GPL licenses: * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html * */ /** * * @description Create a sortable table with multi-column sorting capabilitys * * @example $('table').tablesorter(); * @desc Create a simple tablesorter interface. * * @example $('table').tablesorter({ sortList:[[0,0],[1,0]] }); * @desc Create a tablesorter interface and sort on the first and secound column column headers. * * @example $('table').tablesorter({ headers: { 0: { sorter: false}, 1: {sorter: false} } }); * * @desc Create a tablesorter interface and disableing the first and second column headers. * * * @example $('table').tablesorter({ headers: { 0: {sorter:"integer"}, 1: {sorter:"currency"} } }); * * @desc Create a tablesorter interface and set a column parser for the first * and second column. * * * @param Object * settings An object literal containing key/value pairs to provide * optional settings. * * * @option String cssHeader (optional) A string of the class name to be appended * to sortable tr elements in the thead of the table. Default value: * "header" * * @option String cssAsc (optional) A string of the class name to be appended to * sortable tr elements in the thead on a ascending sort. Default value: * "headerSortUp" * * @option String cssDesc (optional) A string of the class name to be appended * to sortable tr elements in the thead on a descending sort. Default * value: "headerSortDown" * * @option String sortInitialOrder (optional) A string of the inital sorting * order can be asc or desc. Default value: "asc" * * @option String sortMultisortKey (optional) A string of the multi-column sort * key. Default value: "shiftKey" * * @option String textExtraction (optional) A string of the text-extraction * method to use. For complex html structures inside td cell set this * option to "complex", on large tables the complex option can be slow. * Default value: "simple" * * @option Object headers (optional) An object of instructions for per-column * controls in the format: headers: { 0: { option: setting }, ... }. For * example, to disable sorting on the first two columns of a table: * headers: { 0: { sorter: false}, 1: {sorter: false} }. * Default value: null. * * @option Array sortList (optional) An array of instructions for per-column sorting * and direction in the format: [[columnIndex, sortDirection], ... ] where * columnIndex is a zero-based index for your columns left-to-right and * sortDirection is 0 for Ascending and 1 for Descending. A valid argument * that sorts ascending first by column 1 and then column 2 looks like: * [[0,0],[1,0]]. Default value: null. * * @option Array sortForce (optional) An array containing forced sorting rules. * Use to add an additional forced sort that will be appended to the dynamic * selections by the user. For example, can be used to sort people alphabetically * after some other user-selected sort that results in rows with the same value * like dates or money due. It can help prevent data from appearing as though it * has a random secondary sort. Default value: null. * * @option Boolean sortLocaleCompare (optional) Boolean flag indicating whatever * to use String.localeCampare method or not. Default set to true. * * * @option Array sortAppend (optional) An array containing forced sorting rules. * This option let's you specify a default sorting rule, which is * appended to user-selected rules. Default value: null * * @option Boolean widthFixed (optional) Boolean flag indicating if tablesorter * should apply fixed widths to the table columns. This is usefull when * using the pager companion plugin. This options requires the dimension * jquery plugin. Default value: false * * @option Boolean cancelSelection (optional) Boolean flag indicating if * tablesorter should cancel selection of the table headers text. * Default value: true * * @option Boolean debug (optional) Boolean flag indicating if tablesorter * should display debuging information usefull for development. * * @type jQuery * * @name tablesorter * * @cat Plugins/Tablesorter * * @author Christian Bach/christian.bach@polyester.se */ (function ($) { $.extend({ tablesorter: new function () { var parsers = [], widgets = []; this.defaults = { cssHeader: "header", cssAsc: "headerSortUp", cssDesc: "headerSortDown", cssChildRow: "expand-child", sortInitialOrder: "asc", sortMultiSortKey: "shiftKey", sortForce: null, sortAppend: null, sortLocaleCompare: true, textExtraction: "simple", parsers: {}, widgets: [], widgetZebra: { css: ["even", "odd"] }, headers: {}, widthFixed: false, cancelSelection: true, sortList: [], headerList: [], dateFormat: "us", decimal: '/\.|\,/g', onRenderHeader: null, selectorHeaders: 'thead th', debug: false }; /* debuging utils */ function benchmark(s, d) { log(s + "," + (new Date().getTime() - d.getTime()) + "ms"); } this.benchmark = benchmark; function log(s) { if (typeof console != "undefined" && typeof console.debug != "undefined") { console.log(s); } else { alert(s); } } /* parsers utils */ function buildParserCache(table, $headers) { if (table.config.debug) { var parsersDebug = ""; } if (table.tBodies.length == 0) return; // In the case of empty tables var rows = table.tBodies[0].rows; if (rows[0]) { var list = [], cells = rows[0].cells, l = cells.length; for (var i = 0; i < l; i++) { var p = false; if ($.metadata && ($($headers[i]).metadata() && $($headers[i]).metadata().sorter)) { p = getParserById($($headers[i]).metadata().sorter); } else if ((table.config.headers[i] && table.config.headers[i].sorter)) { p = getParserById(table.config.headers[i].sorter); } if (!p) { p = detectParserForColumn(table, rows, -1, i); } if (table.config.debug) { parsersDebug += "column:" + i + " parser:" + p.id + "\n"; } list.push(p); } } if (table.config.debug) { log(parsersDebug); } return list; }; function detectParserForColumn(table, rows, rowIndex, cellIndex) { var l = parsers.length, node = false, nodeValue = false, keepLooking = true; while (nodeValue == '' && keepLooking) { rowIndex++; if (rows[rowIndex]) { node = getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex); nodeValue = trimAndGetNodeText(table.config, node); if (table.config.debug) { log('Checking if value was empty on row:' + rowIndex); } } else { keepLooking = false; } } for (var i = 1; i < l; i++) { if (parsers[i].is(nodeValue, table, node)) { return parsers[i]; } } // 0 is always the generic parser (text) return parsers[0]; } function getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex) { return rows[rowIndex].cells[cellIndex]; } function trimAndGetNodeText(config, node) { return $.trim(getElementText(config, node)); } function getParserById(name) { var l = parsers.length; for (var i = 0; i < l; i++) { if (parsers[i].id.toLowerCase() == name.toLowerCase()) { return parsers[i]; } } return false; } /* utils */ function buildCache(table) { if (table.config.debug) { var cacheTime = new Date(); } var totalRows = (table.tBodies[0] && table.tBodies[0].rows.length) || 0, totalCells = (table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length) || 0, parsers = table.config.parsers, cache = { row: [], normalized: [] }; for (var i = 0; i < totalRows; ++i) { /** Add the table data to main data array */ var c = $(table.tBodies[0].rows[i]), cols = []; // if this is a child row, add it to the last row's children and // continue to the next row if (c.hasClass(table.config.cssChildRow)) { cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add(c); // go to the next for loop continue; } cache.row.push(c); for (var j = 0; j < totalCells; ++j) { cols.push(parsers[j].format(getElementText(table.config, c[0].cells[j]), table, c[0].cells[j])); } cols.push(cache.normalized.length); // add position for rowCache cache.normalized.push(cols); cols = null; }; if (table.config.debug) { benchmark("Building cache for " + totalRows + " rows:", cacheTime); } return cache; }; function getElementText(config, node) { if (!node) return ""; var $node = $(node), data = $node.attr('data-sort-value'); if (data !== undefined) return data; var text = ""; if (!config.supportsTextContent) config.supportsTextContent = node.textContent || false; if (config.textExtraction == "simple") { if (config.supportsTextContent) { text = node.textContent; } else { if (node.childNodes[0] && node.childNodes[0].hasChildNodes()) { text = node.childNodes[0].innerHTML; } else { text = node.innerHTML; } } } else { if (typeof(config.textExtraction) == "function") { text = config.textExtraction(node); } else { text = $(node).text(); } } return text; } function appendToTable(table, cache) { if (table.config.debug) { var appendTime = new Date() } var c = cache, r = c.row, n = c.normalized, totalRows = n.length, checkCell = (n[0].length - 1), tableBody = $(table.tBodies[0]), rows = []; for (var i = 0; i < totalRows; i++) { var pos = n[i][checkCell]; rows.push(r[pos]); if (!table.config.appender) { //var o = ; var l = r[pos].length; for (var j = 0; j < l; j++) { tableBody[0].appendChild(r[pos][j]); } // } } if (table.config.appender) { table.config.appender(table, rows); } rows = null; if (table.config.debug) { benchmark("Rebuilt table:", appendTime); } // apply table widgets applyWidget(table); // trigger sortend setTimeout(function () { $(table).trigger("sortEnd"); }, 0); }; function buildHeaders(table) { if (table.config.debug) { var time = new Date(); } var meta = ($.metadata) ? true : false; var header_index = computeTableHeaderCellIndexes(table); var $tableHeaders = $(table.config.selectorHeaders, table).each(function (index) { this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; // this.column = index; this.order = formatSortingOrder(table.config.sortInitialOrder); this.count = this.order; if (checkHeaderMetadata(this) || checkHeaderOptions(table, index)) this.sortDisabled = true; if (checkHeaderOptionsSortingLocked(table, index)) this.order = this.lockedOrder = checkHeaderOptionsSortingLocked(table, index); if (!this.sortDisabled) { var $th = $(this).addClass(table.config.cssHeader); if (table.config.onRenderHeader) table.config.onRenderHeader.apply($th); } // add cell to headerList table.config.headerList[index] = this; }); if (table.config.debug) { benchmark("Built headers:", time); log($tableHeaders); } return $tableHeaders; }; // from: // http://www.javascripttoolbox.com/lib/table/examples.php // http://www.javascripttoolbox.com/temp/table_cellindex.html function computeTableHeaderCellIndexes(t) { var matrix = []; var lookup = {}; var thead = t.getElementsByTagName('THEAD')[0]; var trs = thead.getElementsByTagName('TR'); for (var i = 0; i < trs.length; i++) { var cells = trs[i].cells; for (var j = 0; j < cells.length; j++) { var c = cells[j]; var rowIndex = c.parentNode.rowIndex; var cellId = rowIndex + "-" + c.cellIndex; var rowSpan = c.rowSpan || 1; var colSpan = c.colSpan || 1 var firstAvailCol; if (typeof(matrix[rowIndex]) == "undefined") { matrix[rowIndex] = []; } // Find first available column in the first row for (var k = 0; k < matrix[rowIndex].length + 1; k++) { if (typeof(matrix[rowIndex][k]) == "undefined") { firstAvailCol = k; break; } } lookup[cellId] = firstAvailCol; for (var k = rowIndex; k < rowIndex + rowSpan; k++) { if (typeof(matrix[k]) == "undefined") { matrix[k] = []; } var matrixrow = matrix[k]; for (var l = firstAvailCol; l < firstAvailCol + colSpan; l++) { matrixrow[l] = "x"; } } } } return lookup; } function checkCellColSpan(table, rows, row) { var arr = [], r = table.tHead.rows, c = r[row].cells; for (var i = 0; i < c.length; i++) { var cell = c[i]; if (cell.colSpan > 1) { arr = arr.concat(checkCellColSpan(table, headerArr, row++)); } else { if (table.tHead.length == 1 || (cell.rowSpan > 1 || !r[row + 1])) { arr.push(cell); } // headerArr[row] = (i+row); } } return arr; }; function checkHeaderMetadata(cell) { if (($.metadata) && ($(cell).metadata().sorter === false)) { return true; }; return false; } function checkHeaderOptions(table, i) { if ((table.config.headers[i]) && (table.config.headers[i].sorter === false)) { return true; }; return false; } function checkHeaderOptionsSortingLocked(table, i) { if ((table.config.headers[i]) && (table.config.headers[i].lockedOrder)) return table.config.headers[i].lockedOrder; return false; } function applyWidget(table) { var c = table.config.widgets; var l = c.length; for (var i = 0; i < l; i++) { getWidgetById(c[i]).format(table); } } function getWidgetById(name) { var l = widgets.length; for (var i = 0; i < l; i++) { if (widgets[i].id.toLowerCase() == name.toLowerCase()) { return widgets[i]; } } }; function formatSortingOrder(v) { if (typeof(v) != "Number") { return (v.toLowerCase() == "desc") ? 1 : 0; } else { return (v == 1) ? 1 : 0; } } function isValueInArray(v, a) { var l = a.length; for (var i = 0; i < l; i++) { if (a[i][0] == v) { return true; } } return false; } function setHeadersCss(table, $headers, list, css) { // remove all header information $headers.removeClass(css[0]).removeClass(css[1]); var h = []; $headers.each(function (offset) { if (!this.sortDisabled) { h[this.column] = $(this); } }); var l = list.length; for (var i = 0; i < l; i++) { h[list[i][0]].addClass(css[list[i][1]]); } } function fixColumnWidth(table, $headers) { var c = table.config; if (c.widthFixed) { var colgroup = $(''); $("tr:first td", table.tBodies[0]).each(function () { colgroup.append($('').css('width', $(this).width())); }); $(table).prepend(colgroup); }; } function updateHeaderSortCount(table, sortList) { var c = table.config, l = sortList.length; for (var i = 0; i < l; i++) { var s = sortList[i], o = c.headerList[s[0]]; o.count = s[1]; o.count++; } } /* sorting methods */ var sortWrapper; function multisort(table, sortList, cache) { if (table.config.debug) { var sortTime = new Date(); } var dynamicExp = "sortWrapper = function(a,b) {", l = sortList.length; // TODO: inline functions. for (var i = 0; i < l; i++) { var c = sortList[i][0]; var order = sortList[i][1]; // var s = (getCachedSortType(table.config.parsers,c) == "text") ? // ((order == 0) ? "sortText" : "sortTextDesc") : ((order == 0) ? // "sortNumeric" : "sortNumericDesc"); // var s = (table.config.parsers[c].type == "text") ? ((order == 0) // ? makeSortText(c) : makeSortTextDesc(c)) : ((order == 0) ? // makeSortNumeric(c) : makeSortNumericDesc(c)); var s = (table.config.parsers[c].type == "text") ? ((order == 0) ? makeSortFunction("text", "asc", c) : makeSortFunction("text", "desc", c)) : ((order == 0) ? makeSortFunction("numeric", "asc", c) : makeSortFunction("numeric", "desc", c)); var e = "e" + i; dynamicExp += "var " + e + " = " + s; // + "(a[" + c + "],b[" + c // + "]); "; dynamicExp += "if(" + e + ") { return " + e + "; } "; dynamicExp += "else { "; } // if value is the same keep orignal order var orgOrderCol = cache.normalized[0].length - 1; dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];"; for (var i = 0; i < l; i++) { dynamicExp += "}; "; } dynamicExp += "return 0; "; dynamicExp += "}; "; if (table.config.debug) { benchmark("Evaling expression:" + dynamicExp, new Date()); } eval(dynamicExp); cache.normalized.sort(sortWrapper); if (table.config.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time:", sortTime); } return cache; }; function makeSortFunction(type, direction, index) { var a = "a[" + index + "]", b = "b[" + index + "]"; if (type == 'text' && direction == 'asc') { return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + a + " < " + b + ") ? -1 : 1 )));"; } else if (type == 'text' && direction == 'desc') { return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + b + " < " + a + ") ? -1 : 1 )));"; } else if (type == 'numeric' && direction == 'asc') { return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + a + " - " + b + "));"; } else if (type == 'numeric' && direction == 'desc') { return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + b + " - " + a + "));"; } }; function makeSortText(i) { return "((a[" + i + "] < b[" + i + "]) ? -1 : ((a[" + i + "] > b[" + i + "]) ? 1 : 0));"; }; function makeSortTextDesc(i) { return "((b[" + i + "] < a[" + i + "]) ? -1 : ((b[" + i + "] > a[" + i + "]) ? 1 : 0));"; }; function makeSortNumeric(i) { return "a[" + i + "]-b[" + i + "];"; }; function makeSortNumericDesc(i) { return "b[" + i + "]-a[" + i + "];"; }; function sortText(a, b) { if (table.config.sortLocaleCompare) return a.localeCompare(b); return ((a < b) ? -1 : ((a > b) ? 1 : 0)); }; function sortTextDesc(a, b) { if (table.config.sortLocaleCompare) return b.localeCompare(a); return ((b < a) ? -1 : ((b > a) ? 1 : 0)); }; function sortNumeric(a, b) { return a - b; }; function sortNumericDesc(a, b) { return b - a; }; function getCachedSortType(parsers, i) { return parsers[i].type; }; /* public methods */ this.construct = function (settings) { return this.each(function () { // if no thead or tbody quit. if (!this.tHead || !this.tBodies) return; // declare var $this, $document, $headers, cache, config, shiftDown = 0, sortOrder; // new blank config object this.config = {}; // merge and extend. config = $.extend(this.config, $.tablesorter.defaults, settings); // store common expression for speed $this = $(this); // save the settings where they read $.data(this, "tablesorter", config); // build headers $headers = buildHeaders(this); // try to auto detect column type, and store in tables config this.config.parsers = buildParserCache(this, $headers); // build the cache for the tbody cells cache = buildCache(this); // get the css class names, could be done else where. var sortCSS = [config.cssDesc, config.cssAsc]; // fixate columns if the users supplies the fixedWidth option fixColumnWidth(this); // apply event handling to headers // this is to big, perhaps break it out? $headers.click( function (e) { var totalRows = ($this[0].tBodies[0] && $this[0].tBodies[0].rows.length) || 0; if (!this.sortDisabled && totalRows > 0) { // Only call sortStart if sorting is // enabled. $this.trigger("sortStart"); // store exp, for speed var $cell = $(this); // get current column index var i = this.column; // get current column sort order this.order = this.count++ % 2; // always sort on the locked order. if(this.lockedOrder) this.order = this.lockedOrder; // user only whants to sort on one // column if (!e[config.sortMultiSortKey]) { // flush the sort list config.sortList = []; if (config.sortForce != null) { var a = config.sortForce; for (var j = 0; j < a.length; j++) { if (a[j][0] != i) { config.sortList.push(a[j]); } } } // add column to sort list config.sortList.push([i, this.order]); // multi column sorting } else { // the user has clicked on an all // ready sortet column. if (isValueInArray(i, config.sortList)) { // revers the sorting direction // for all tables. for (var j = 0; j < config.sortList.length; j++) { var s = config.sortList[j], o = config.headerList[s[0]]; if (s[0] == i) { o.count = s[1]; o.count++; s[1] = o.count % 2; } } } else { // add column to sort list array config.sortList.push([i, this.order]); } }; setTimeout(function () { // set css for headers setHeadersCss($this[0], $headers, config.sortList, sortCSS); appendToTable( $this[0], multisort( $this[0], config.sortList, cache) ); }, 1); // stop normal event by returning false return false; } // cancel selection }).mousedown(function () { if (config.cancelSelection) { this.onselectstart = function () { return false }; return false; } }); // apply easy methods that trigger binded events $this.bind("update", function () { var me = this; setTimeout(function () { // rebuild parsers. me.config.parsers = buildParserCache( me, $headers); // rebuild the cache map cache = buildCache(me); }, 1); }).bind("updateCell", function (e, cell) { var config = this.config; // get position from the dom. var pos = [(cell.parentNode.rowIndex - 1), cell.cellIndex]; // update cache cache.normalized[pos[0]][pos[1]] = config.parsers[pos[1]].format( getElementText(config, cell), cell); }).bind("sorton", function (e, list) { $(this).trigger("sortStart"); config.sortList = list; // update and store the sortlist var sortList = config.sortList; // update header count index updateHeaderSortCount(this, sortList); // set css for headers setHeadersCss(this, $headers, sortList, sortCSS); // sort the table and append it to the dom appendToTable(this, multisort(this, sortList, cache)); }).bind("appendCache", function () { appendToTable(this, cache); }).bind("applyWidgetId", function (e, id) { getWidgetById(id).format(this); }).bind("applyWidgets", function () { // apply widgets applyWidget(this); }); if ($.metadata && ($(this).metadata() && $(this).metadata().sortlist)) { config.sortList = $(this).metadata().sortlist; } // if user has supplied a sort list to constructor. if (config.sortList.length > 0) { $this.trigger("sorton", [config.sortList]); } // apply widgets applyWidget(this); }); }; this.addParser = function (parser) { var l = parsers.length, a = true; for (var i = 0; i < l; i++) { if (parsers[i].id.toLowerCase() == parser.id.toLowerCase()) { a = false; } } if (a) { parsers.push(parser); }; }; this.addWidget = function (widget) { widgets.push(widget); }; this.formatFloat = function (s) { var i = parseFloat(s); return (isNaN(i)) ? 0 : i; }; this.formatInt = function (s) { var i = parseInt(s); return (isNaN(i)) ? 0 : i; }; this.isDigit = function (s, config) { // replace all an wanted chars and match. return /^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g, ''))); }; this.clearTableBody = function (table) { if ($.browser.msie) { while (table.tBodies[0].firstChild) { table.tBodies[0].removeChild(table.tBodies[0].firstChild); } } else { table.tBodies[0].innerHTML = ""; } }; } }); // extend plugin scope $.fn.extend({ tablesorter: $.tablesorter.construct }); // make shortcut var ts = $.tablesorter; // add default parsers ts.addParser({ id: "text", is: function (s) { return true; }, format: function (s) { return $.trim(s.toLocaleLowerCase()); }, type: "text" }); ts.addParser({ id: "digit", is: function (s, table) { var c = table.config; return $.tablesorter.isDigit(s, c); }, format: function (s) { return $.tablesorter.formatFloat(s); }, type: "numeric" }); ts.addParser({ id: "currency", is: function (s) { return /^[£$€?.]/.test(s); }, format: function (s) { return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g), "")); }, type: "numeric" }); ts.addParser({ id: "ipAddress", is: function (s) { return /^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s); }, format: function (s) { var a = s.split("."), r = "", l = a.length; for (var i = 0; i < l; i++) { var item = a[i]; if (item.length == 2) { r += "0" + item; } else { r += item; } } return $.tablesorter.formatFloat(r); }, type: "numeric" }); ts.addParser({ id: "url", is: function (s) { return /^(https?|ftp|file):\/\/$/.test(s); }, format: function (s) { return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//), '')); }, type: "text" }); ts.addParser({ id: "isoDate", is: function (s) { return /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s); }, format: function (s) { return $.tablesorter.formatFloat((s != "") ? new Date(s.replace( new RegExp(/-/g), "/")).getTime() : "0"); }, type: "numeric" }); ts.addParser({ id: "percent", is: function (s) { return /\%$/.test($.trim(s)); }, format: function (s) { return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g), "")); }, type: "numeric" }); ts.addParser({ id: "usLongDate", is: function (s) { return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/)); }, format: function (s) { return $.tablesorter.formatFloat(new Date(s).getTime()); }, type: "numeric" }); ts.addParser({ id: "shortDate", is: function (s) { return /\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s); }, format: function (s, table) { var c = table.config; s = s.replace(/\-/g, "/"); if (c.dateFormat == "us") { // reformat the string in ISO format s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$1/$2"); } if (c.dateFormat == "pt") { s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1"); } else if (c.dateFormat == "uk") { // reformat the string in ISO format s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1"); } else if (c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") { s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/, "$1/$2/$3"); } return $.tablesorter.formatFloat(new Date(s).getTime()); }, type: "numeric" }); ts.addParser({ id: "time", is: function (s) { return /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s); }, format: function (s) { return $.tablesorter.formatFloat(new Date("2000/01/01 " + s).getTime()); }, type: "numeric" }); ts.addParser({ id: "metadata", is: function (s) { return false; }, format: function (s, table, cell) { var c = table.config, p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; return $(cell).metadata()[p]; }, type: "numeric" }); // add default widgets ts.addWidget({ id: "zebra", format: function (table) { if (table.config.debug) { var time = new Date(); } var $tr, row = -1, odd; // loop through the visible rows $("tr:visible", table.tBodies[0]).each(function (i) { $tr = $(this); // style children rows the same way the parent // row was styled if (!$tr.hasClass(table.config.cssChildRow)) row++; odd = (row % 2 == 0); $tr.removeClass( table.config.widgetZebra.css[odd ? 0 : 1]).addClass( table.config.widgetZebra.css[odd ? 1 : 0]) }); if (table.config.debug) { $.tablesorter.benchmark("Applying Zebra widget", time); } } }); })(jQuery); ================================================ FILE: src/Profiler/view/base/web/js/lib/jquery.treetable.js ================================================ /* * jQuery treetable Plugin 3.2.0 * http://ludo.cubicphuse.nl/jquery-treetable * * Copyright 2013, Ludo van den Boom * Dual licensed under the MIT or GPL Version 2 licenses. */ (function($) { "use strict"; var Node, Tree, methods; Node = (function() { function Node(row, tree, settings) { var parentId; this.row = row; this.tree = tree; this.settings = settings; // TODO Ensure id/parentId is always a string (not int) this.id = this.row.data(this.settings.nodeIdAttr); // TODO Move this to a setParentId function? parentId = this.row.data(this.settings.parentIdAttr); if (parentId != null && parentId !== "") { this.parentId = parentId; } this.treeCell = $(this.row.children(this.settings.columnElType)[this.settings.column]); this.expander = $(this.settings.expanderTemplate); this.indenter = $(this.settings.indenterTemplate); this.children = []; this.initialized = false; this.treeCell.prepend(this.indenter); } Node.prototype.addChild = function(child) { return this.children.push(child); }; Node.prototype.ancestors = function() { var ancestors, node; node = this; ancestors = []; while (node = node.parentNode()) { ancestors.push(node); } return ancestors; }; Node.prototype.collapse = function() { if (this.collapsed()) { return this; } this.row.removeClass("expanded").addClass("collapsed"); this._hideChildren(); this.expander.attr("title", this.settings.stringExpand); if (this.initialized && this.settings.onNodeCollapse != null) { this.settings.onNodeCollapse.apply(this); } return this; }; Node.prototype.collapsed = function() { return this.row.hasClass("collapsed"); }; // TODO destroy: remove event handlers, expander, indenter, etc. Node.prototype.expand = function() { if (this.expanded()) { return this; } this.row.removeClass("collapsed").addClass("expanded"); if (this.initialized && this.settings.onNodeExpand != null) { this.settings.onNodeExpand.apply(this); } if ($(this.row).is(":visible")) { this._showChildren(); } this.expander.attr("title", this.settings.stringCollapse); return this; }; Node.prototype.expanded = function() { return this.row.hasClass("expanded"); }; Node.prototype.hide = function() { this._hideChildren(); this.row.hide(); return this; }; Node.prototype.isBranchNode = function() { if(this.children.length > 0 || this.row.data(this.settings.branchAttr) === true) { return true; } else { return false; } }; Node.prototype.updateBranchLeafClass = function(){ this.row.removeClass('branch'); this.row.removeClass('leaf'); this.row.addClass(this.isBranchNode() ? 'branch' : 'leaf'); }; Node.prototype.level = function() { return this.ancestors().length; }; Node.prototype.parentNode = function() { if (this.parentId != null) { return this.tree[this.parentId]; } else { return null; } }; Node.prototype.removeChild = function(child) { var i = $.inArray(child, this.children); return this.children.splice(i, 1) }; Node.prototype.render = function() { var handler, settings = this.settings, target; if (settings.expandable === true && this.isBranchNode()) { handler = function(e) { $(this).parents("table").treetable("node", $(this).parents("tr").data(settings.nodeIdAttr)).toggle(); return e.preventDefault(); }; this.indenter.html(this.expander); target = settings.clickableNodeNames === true ? this.treeCell : this.expander; target.off("click.treetable").on("click.treetable", handler); target.off("keydown.treetable").on("keydown.treetable", function(e) { if (e.keyCode == 13) { handler.apply(this, [e]); } }); } this.indenter[0].style.paddingLeft = "" + (this.level() * settings.indent) + "px"; return this; }; Node.prototype.reveal = function() { if (this.parentId != null) { this.parentNode().reveal(); } return this.expand(); }; Node.prototype.setParent = function(node) { if (this.parentId != null) { this.tree[this.parentId].removeChild(this); } this.parentId = node.id; this.row.data(this.settings.parentIdAttr, node.id); return node.addChild(this); }; Node.prototype.show = function() { if (!this.initialized) { this._initialize(); } this.row.show(); if (this.expanded()) { this._showChildren(); } return this; }; Node.prototype.toggle = function() { if (this.expanded()) { this.collapse(); } else { this.expand(); } return this; }; Node.prototype._hideChildren = function() { var child, _i, _len, _ref, _results; _ref = this.children; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { child = _ref[_i]; _results.push(child.hide()); } return _results; }; Node.prototype._initialize = function() { var settings = this.settings; this.render(); if (settings.expandable === true && settings.initialState === "collapsed") { this.collapse(); } else { this.expand(); } if (settings.onNodeInitialized != null) { settings.onNodeInitialized.apply(this); } return this.initialized = true; }; Node.prototype._showChildren = function() { var child, _i, _len, _ref, _results; _ref = this.children; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { child = _ref[_i]; _results.push(child.show()); } return _results; }; return Node; })(); Tree = (function() { function Tree(table, settings) { this.table = table; this.settings = settings; this.tree = {}; // Cache the nodes and roots in simple arrays for quick access/iteration this.nodes = []; this.roots = []; } Tree.prototype.collapseAll = function() { var node, _i, _len, _ref, _results; _ref = this.nodes; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { node = _ref[_i]; _results.push(node.collapse()); } return _results; }; Tree.prototype.expandAll = function() { var node, _i, _len, _ref, _results; _ref = this.nodes; _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { node = _ref[_i]; _results.push(node.expand()); } return _results; }; Tree.prototype.findLastNode = function (node) { if (node.children.length > 0) { return this.findLastNode(node.children[node.children.length - 1]); } else { return node; } }; Tree.prototype.loadRows = function(rows) { var node, row, i; if (rows != null) { for (i = 0; i < rows.length; i++) { row = $(rows[i]); if (row.data(this.settings.nodeIdAttr) != null) { node = new Node(row, this.tree, this.settings); this.nodes.push(node); this.tree[node.id] = node; if (node.parentId != null && this.tree[node.parentId]) { this.tree[node.parentId].addChild(node); } else { this.roots.push(node); } } } } for (i = 0; i < this.nodes.length; i++) { node = this.nodes[i].updateBranchLeafClass(); } return this; }; Tree.prototype.move = function(node, destination) { // Conditions: // 1: +node+ should not be inserted as a child of +node+ itself. // 2: +destination+ should not be the same as +node+'s current parent (this // prevents +node+ from being moved to the same location where it already // is). // 3: +node+ should not be inserted in a location in a branch if this would // result in +node+ being an ancestor of itself. var nodeParent = node.parentNode(); if (node !== destination && destination.id !== node.parentId && $.inArray(node, destination.ancestors()) === -1) { node.setParent(destination); this._moveRows(node, destination); // Re-render parentNode if this is its first child node, and therefore // doesn't have the expander yet. if (node.parentNode().children.length === 1) { node.parentNode().render(); } } if(nodeParent){ nodeParent.updateBranchLeafClass(); } if(node.parentNode()){ node.parentNode().updateBranchLeafClass(); } node.updateBranchLeafClass(); return this; }; Tree.prototype.removeNode = function(node) { // Recursively remove all descendants of +node+ this.unloadBranch(node); // Remove node from DOM () node.row.remove(); // Remove node from parent children list if (node.parentId != null) { node.parentNode().removeChild(node); } // Clean up Tree object (so Node objects are GC-ed) delete this.tree[node.id]; this.nodes.splice($.inArray(node, this.nodes), 1); return this; } Tree.prototype.render = function() { var root, _i, _len, _ref; _ref = this.roots; for (_i = 0, _len = _ref.length; _i < _len; _i++) { root = _ref[_i]; // Naming is confusing (show/render). I do not call render on node from // here. root.show(); } return this; }; Tree.prototype.sortBranch = function(node, sortFun) { // First sort internal array of children node.children.sort(sortFun); // Next render rows in correct order on page this._sortChildRows(node); return this; }; Tree.prototype.unloadBranch = function(node) { // Use a copy of the children array to not have other functions interfere // with this function if they manipulate the children array // (eg removeNode). var children = node.children.slice(0), i; for (i = 0; i < children.length; i++) { this.removeNode(children[i]); } // Reset node's collection of children node.children = []; node.updateBranchLeafClass(); return this; }; Tree.prototype._moveRows = function(node, destination) { var children = node.children, i; node.row.insertAfter(destination.row); node.render(); // Loop backwards through children to have them end up on UI in correct // order (see #112) for (i = children.length - 1; i >= 0; i--) { this._moveRows(children[i], node); } }; // Special _moveRows case, move children to itself to force sorting Tree.prototype._sortChildRows = function(parentNode) { return this._moveRows(parentNode, parentNode); }; return Tree; })(); // jQuery Plugin methods = { init: function(options, force) { var settings; settings = $.extend({ branchAttr: "ttBranch", clickableNodeNames: false, column: 0, columnElType: "td", // i.e. 'td', 'th' or 'td,th' expandable: false, expanderTemplate: " ", indent: 19, indenterTemplate: "", initialState: "collapsed", nodeIdAttr: "ttId", // maps to data-tt-id parentIdAttr: "ttParentId", // maps to data-tt-parent-id stringExpand: "Expand", stringCollapse: "Collapse", // Events onInitialized: null, onNodeCollapse: null, onNodeExpand: null, onNodeInitialized: null }, options); return this.each(function() { var el = $(this), tree; if (force || el.data("treetable") === undefined) { tree = new Tree(this, settings); tree.loadRows(this.rows).render(); el.addClass("treetable").data("treetable", tree); if (settings.onInitialized != null) { settings.onInitialized.apply(tree); } } return el; }); }, destroy: function() { return this.each(function() { return $(this).removeData("treetable").removeClass("treetable"); }); }, collapseAll: function() { this.data("treetable").collapseAll(); return this; }, collapseNode: function(id) { var node = this.data("treetable").tree[id]; if (node) { node.collapse(); } else { throw new Error("Unknown node '" + id + "'"); } return this; }, expandAll: function() { this.data("treetable").expandAll(); return this; }, expandNode: function(id) { var node = this.data("treetable").tree[id]; if (node) { if (!node.initialized) { node._initialize(); } node.expand(); } else { throw new Error("Unknown node '" + id + "'"); } return this; }, loadBranch: function(node, rows) { var settings = this.data("treetable").settings, tree = this.data("treetable").tree; // TODO Switch to $.parseHTML rows = $(rows); if (node == null) { // Inserting new root nodes this.append(rows); } else { var lastNode = this.data("treetable").findLastNode(node); rows.insertAfter(lastNode.row); } this.data("treetable").loadRows(rows); // Make sure nodes are properly initialized rows.filter("tr").each(function() { tree[$(this).data(settings.nodeIdAttr)].show(); }); if (node != null) { // Re-render parent to ensure expander icon is shown (#79) node.render().expand(); } return this; }, move: function(nodeId, destinationId) { var destination, node; node = this.data("treetable").tree[nodeId]; destination = this.data("treetable").tree[destinationId]; this.data("treetable").move(node, destination); return this; }, node: function(id) { return this.data("treetable").tree[id]; }, removeNode: function(id) { var node = this.data("treetable").tree[id]; if (node) { this.data("treetable").removeNode(node); } else { throw new Error("Unknown node '" + id + "'"); } return this; }, reveal: function(id) { var node = this.data("treetable").tree[id]; if (node) { node.reveal(); } else { throw new Error("Unknown node '" + id + "'"); } return this; }, sortBranch: function(node, columnOrFunction) { var settings = this.data("treetable").settings, prepValue, sortFun; columnOrFunction = columnOrFunction || settings.column; sortFun = columnOrFunction; if ($.isNumeric(columnOrFunction)) { sortFun = function(a, b) { var extractValue, valA, valB; extractValue = function(node) { var val = node.row.find("td:eq(" + columnOrFunction + ")").text(); // Ignore trailing/leading whitespace and use uppercase values for // case insensitive ordering return $.trim(val).toUpperCase(); } valA = extractValue(a); valB = extractValue(b); if (valA < valB) return -1; if (valA > valB) return 1; return 0; }; } this.data("treetable").sortBranch(node, sortFun); return this; }, unloadBranch: function(node) { this.data("treetable").unloadBranch(node); return this; } }; $.fn.treetable = function(method) { if (methods[method]) { return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); } else if (typeof method === 'object' || !method) { return methods.init.apply(this, arguments); } else { return $.error("Method " + method + " does not exist on jQuery.treetable"); } }; // Expose classes to world window.TreeTable || (window.TreeTable = {}); window.TreeTable.Node = Node; window.TreeTable.Tree = Tree; })(jQuery); ================================================ FILE: src/Profiler/view/base/web/js/table.js ================================================ define([ 'jquery', 'jquery/jquery.cookie', 'jquery/ui', 'Mirasvit_Profiler/js/lib/jquery.treetable', 'Mirasvit_Profiler/js/lib/jquery.tablesorter', 'Mirasvit_Profiler/js/lib/jquery.filtertable' ], function ($) { 'use strict'; var methods = { init: function (options) { var $table = this; $table.treetable({ expandable: true }); $table.tablesorter({ callback: function (term, table) { $table.treetable('expandAll'); } }); $table.bind("sortStart", function () { $table.treetable('expandAll'); }); $table.filterTable({ callback: function (term, table) { $table.treetable('expandAll'); } }); if ($table.attr('data-threshold')) { var max = 0; var min = 1000000; $('[data-threshold-value]', $table).each(function (i, tr) { var value = parseInt($(tr).attr('data-threshold-value')); if (value > max) { max = value; } if (value < max) { min = value; } }); var $threshold = $($table.attr('data-threshold')); $threshold.slider({ min: min, max: max, value: min, slide: function (event, ui) { $table.treetable('expandAll'); $('[data-threshold-value]', $table).each(function (i, tr) { var value = $(tr).attr('data-threshold-value'); if (value > ui.value) { $(tr).show(); } else { $(tr).hide(); } }); $(".value", this).html(ui.value + " ms"); } }); } } }; $.fn.profilerTable = function (method) { if (methods[method]) { return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); } else if (typeof method === 'object' || !method) { return methods.init.apply(this, arguments); } }; });