* Changed minimum requirement and namespace.

* Minimum PHP is now 7.1
* Namespace is now \Amnuts\Opcache

* Moved service to new location.

* Now at \Amnuts\Opcache\Service
* Added parameter and return types

* Called Service in main index.php file

* Relocated jsx source

* Renamed jsx file

* Moved css to the start of a scss file

* Beefed up package.json file a bit.

* Added compile commands for jsx and css
* Added node-sass as a dependency
* Added various other bits of info

* Building the compiled version.

* Added template of the output
* Added build script
* Added composer run command to build the main index.php file

* Trim contents before outputting

* Make css output compressed

* Better building of the single file.

* Service class now pulled into the single file (no need to install anything via composer)
* Build file tweaked and template file renamed
* Ignore file updated for new template name

* Newly compiled version of the script.

* Relocated jsx/scss build files

* Updated readme

* Updated ignore file and added missing files

* V3 all react (#55)

* Moving interface to only use react.

This is the start of using only react to render the interface.  It'll have slightly revamped react components to fit in with new react js library.  Long way to go yet...

* Further tweaks

* Added rendering of functions as its own component
* Removed version and functions info from being passed in as properties - these were in the 'opstate' anyway
* Updated access to opstate.version where required

* Graphs fixed plus some other tweaks

* Graphs are now react components
* Rearranged components to be a little more logical (to me, anyway)
* Fixed styling issues with widgets if not using graphs
* UsageGraph is simpler but have added Canvas and PureCanvas to compliment the component
* Removed jquery from template file

* Started to get file list rendering again

* Sorted out displaying of files and pagination

* Changed the pagination arrows

* Used sass variables for colours to make it easier to change.

* changed pagination arrows... again.

* Got filtering working

* Added debounce function
* Added `per_page` option for pagination (set to false if you don't want pagination)
* Corrected key handling for pagination fragments
* Added refresh flag to force the pagination to refresh when filtering
* Updated missing default config values in Service class

* Tabs now just hide rather than being unmounted

* Added back icons for reset/realtime tabs

* Made reset cache tab functional again

* Moved interface to using background inline svgs rather than inline pngs

* Better check on json request in Server class & additional permission check

* Getting a bit closer to the realtime update being added back in.

* Included axios for requests
* Started to update Interface with the realtime updating
* Pass in refresh rate as core property

* Sorting out of realtime timer and animation

* Nav icons now use colour variables.

* Simplified GeneralInfo component

* Simplified Directives component

* Specific about what props are sent to Functions component

* Simplified OVerview component and was specific with props.

* Simplified MainNavigation component

* Started to move graphs over to use SVG rather than canvas.

* Tweaked comment

* Graph track colour and large text (if no graph) taken from variables
Was specific about what props to send to Files list

* Removed old Canvas components

* Better handling (as in, it now works!) of file details auto-updating

* Invalidate link now working.

Last bit of jquery gone with this commit!!

* Reset link now respect live updating

If the live updates are turned on then the reset tab will send an ajax request rather than reload the page.  The reset indicator has the same treatment as the live update (goes green by default and pulses) until the next live update where it refreshes the content and removes the 'working' indicator from the reset.

* Some tidy-ups

* Ignored (blacklisted) file paths now shown

* Changed `Files` and `File` to `CachedFiles` and `CachedFile` (respectively)
* Added `IgnoredFiles`
* Removed functionality and additional class to alternative table rows
* Properly filter tabs in case any are not to show

* Added ability to invalidate all files found in search.

Also revamped the Service::handler method to make it much cleaner.

* Added preload file list information

* Started readme update

* Toggling the realtime update will toggle the text

* Some tab indexing

* Updated README

* Added more to readme

* Added cookie functionality back in to remember realtime state between reloads

* Tweaked styles
This commit is contained in:
Andrew Collington 2020-09-19 00:30:17 +01:00 committed by GitHub
parent c6ee88996c
commit 0185ee1cd6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 3394 additions and 1437 deletions

5
.gitignore vendored
View file

@ -4,3 +4,8 @@
/.module-cache /.module-cache
/node_modules /node_modules
/package-lock.json /package-lock.json
/build/*
!/build/template.phps
!/build/build.php
!/build/_frontend
/vendor

143
README.md
View file

@ -1,11 +1,13 @@
# opcache-gui # opcache-gui
A clean and responsive interface for Zend OPcache information, showing statistics, settings and cached files, and providing a real-time update for the information (using jQuery and React). A clean and responsive interface for Zend OPcache information, showing statistics, settings and cached files, and providing a real-time update for the information.
This interface uses ReactJS and Axios and is for modern browsers.
[![Flattr this git repo](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=acollington&url=https://github.com/amnuts/opcache-gui&title=opcache-gui&language=&tags=github&category=software) [![Flattr this git repo](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=acollington&url=https://github.com/amnuts/opcache-gui&title=opcache-gui&language=&tags=github&category=software)
If you like this software or find it helpful then maybe you'll consider supporting my efforts in some way by [signing up to Flattr and leaving a micro-donation](https://flattr.com/@acollington). If you like this software or find it helpful then maybe you'll consider supporting my efforts in some way by [signing up to Flattr and leaving a micro-donation](https://flattr.com/@acollington).
### Getting started ### Using the opcache-gui
There are two ways to getting started using this gui: There are two ways to getting started using this gui:
@ -17,26 +19,28 @@ The easiest way to start using the opcache-gui is to clone this repo, or simply
You can include the files with [Composer](https://getcomposer.org/) by running the command `composer require amnuts/opcache-gui`. You can include the files with [Composer](https://getcomposer.org/) by running the command `composer require amnuts/opcache-gui`.
Once in your `vendor` directory, there are numerous ways in which you can use the interface. For example if you're using a framework such as Symfony or Laravel, you could load opcache-gui into a `Controller`. Your requirements of setting it up within a framework will vary, and wouldn't be possible to detail how to do that within this readme. Once in your `vendor` directory, there are numerous ways in which you can use the interface. For example if you're using a framework such as Symfony or Laravel, you could load opcache-gui into a `Controller`. Your requirements of setting it up within your framework of choice will vary, so it's not really possible to detail how to do that within this readme... but I have faith in your ability to figure it out!
The namespace used for the class is `OpcacheGui`, so once the dependency is in your `autoload.php` you can use the `\OpcacheGui\OpCacheService` class. For example, you could do something like: The namespace used for the class is `Amnuts\Opcache`, so once the dependency is in your `autoload.php` you can use the `\Amnuts\Opcache\Service` class. For example, you could do something like:
```php ```php
<?php <?php
use Amnuts\Opcache\Service;
// assuming location of: /var/www/html/opcache.php // assuming location of: /var/www/html/opcache.php
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
// specify your options (see next section) // specify any options you want different from the defaults, if any
$options = [/* ... */]; $options = [/* ... */];
// setup the class // setup the class and pass in your options, if you have any
\OpcacheGui\OpCacheService::init($options); $opcache = (new Service($options))->handle();
``` ```
And then you can create whatever view you want to show the details. Then you can create whatever view you want with which to show the opcache details. Although there is a pretty neat React-based interface available for you in this repo.
Alternatively include `vendor/amnuts/opcache-gui/index.php` directly to use the default `$options`: Alternatively, include `vendor/amnuts/opcache-gui/index.php` directly and this'll give you the same result as just copying/pasting the `index.php` somewhere.
```php ```php
<?php <?php
@ -46,68 +50,127 @@ Alternatively include `vendor/amnuts/opcache-gui/index.php` directly to use the
require_once __DIR__ . '/../vendor/amnuts/opcache-gui/index.php'; require_once __DIR__ . '/../vendor/amnuts/opcache-gui/index.php';
``` ```
Or you could simple copy or create a symlink to the `index.php` in the vendor directory: You could even simply create a symlink to the `index.php` that's in the `vendor` directory:
```shell script ```shell script
ln -s /var/www/vendor/amnuts/opcache-gui/index.php /var/www/html/opcache.php ln -s /var/www/vendor/amnuts/opcache-gui/index.php /var/www/html/opcache.php
``` ```
Basically, there are plenty of ways to get the interface up and running - pick whichever suits your needs.
### Configuration ### Configuration
If you want to set the configuration options just alter the array at the top of the `index.php` script: The default configuration for the interface looks like this:
```php ```php
$options = [ $options = [
'allow_filelist' => true, // show/hide the files tab 'allow_filelist' => true, // show/hide the files tab
'allow_invalidate' => true, // give a link to invalidate files 'allow_invalidate' => true, // give a link to invalidate files
'allow_reset' => true, // give option to reset the whole cache 'allow_reset' => true, // give option to reset the whole cache
'allow_realtime' => true, // give option to enable/disable real-time updates 'allow_realtime' => true, // give option to enable/disable real-time updates
'refresh_time' => 5, // how often the data will refresh, in seconds 'refresh_time' => 5, // how often the data will refresh, in seconds
'size_precision' => 2, // Digits after decimal point 'size_precision' => 2, // Digits after decimal point
'size_space' => false, // have '1MB' or '1 MB' when showing sizes 'size_space' => false, // have '1MB' or '1 MB' when showing sizes
'charts' => true, // show gauge chart or just big numbers 'charts' => true, // show gauge chart or just big numbers
'debounce_rate' => 250, // milliseconds after key press to send keyup event when filtering 'debounce_rate' => 250, // milliseconds after key press to send keyup event when filtering
'cookie_name' => 'opcachegui', // name of cookie 'per_page' => 200, // How many results per page to show in the file list, false for no pagination
'cookie_ttl' => 365, // days to store cookie 'cookie_name' => 'opcachegui', // name of cookie
'highlight' => [ // highlight charts/big numbers 'cookie_ttl' => 365, // days to store cookie
'memory' => true, 'highlight' => [
'hits' => true, 'memory' => true, // show the memory chart/big number
'keys' => true 'hits' => true, // show the hit rate chart/big number
] 'keys' => true // show the keys used chart/big number
]
]; ];
``` ```
If you want to change any of the defaults, you can pass in just the ones you want to change if you're happy to keep the rest as-is. Just alter the array at the top of the `index.php` script (or pass in the array differently to the `Service` class). For example, the following would change only the `allow_reset` and `refresh_time` values but keep everything else as the default:
```php
$opcache = (new Service([
'refresh_time' => 2,
'allow_reset' => false
]))->handle();
```
### Changing the look
The interface has been split up to allow you to easily change the colours of the gui, or even the core components, should you wish.
The CSS for the interface is in the `build/_frontend/interface.scss` file. If you want to change the interface itself, update the `build/_frontend/interface.jsx` file - it's basically a set of ReactJS components.
If you update those files, you will want to build the interface again and have the new jsx/css put into use. To do that, run the command `php ./build/build.php` from the repo root (you will need `nodejs` and `npm` installed). Once running, you should see the output:
```
🐢 Installing node modules
🏗️ Building js and css
🚀 Creating single build file
💯 Done!
```
The build script will only need to install the `node_modules` once, so on subsequent builds it should be a fair bit quicker!
The build process will create a compiled css file at `build/interface.css` and the javascript of the interface will be in `build/interface.js`. You could probably use both of these within your own frameworks and templating systems, should you wish.
The core PHP template used in the build process, and that acts to pass various bits of data to the ReactJS side of things, is located at `build/template.phps`. If you wanted to update the version of ReactJS used, or how the wrapper html is structured, then this would be the file you'd want to update.
### Overview ### Overview
The overview will show you all the core information. From here you'll be able to see what host and platform you're running on, what version of OPcache you're using, when it was last reset, the functions that are available, all the directives and all the statistics associated with the OPcache (number of hits, memory used, free and wasted memory, etc.) The overview will show you all the core information. From here you'll be able to see what host and platform you're running on, what version of OPcache you're using, when it was last reset, the functions that are available, all the directives and all the statistics associated with the OPcache (number of hits, memory used, free and wasted memory, etc.)
![Overview](http://amnuts.com/images/opcache/screenshot/overview-v2.5.0.png) ![Screenshot of the Overview tab](http://amnuts.com/images/opcache/screenshot/overview-v3.0.0.png)
### File usage ### Cached files
All the files currently in the cache are listed here with their associated statistics. You can filter the results very easily to key in on the particular scripts you're looking for, and you can optionally set levels of the path to be hidden (handy if they all share a common root and you don't want that displayed). It will also indicate if the file cache has expired. All the files currently in the cache are listed here with their associated statistics.
You can filter the results to help find the particular scripts you're looking for, and you can optionally set levels of the path to be hidden. From here you can invalidate the cache for individual files or invalidate the cache for all the files matching your search.
If you do not want to show the file list at all then you can use the `allow_filelist` configuration option; setting it to `false` will suppress the file list altogether. If you do not want to show the file list at all then you can use the `allow_filelist` configuration option; setting it to `false` will suppress the file list altogether.
![File list showing filtered results](http://amnuts.com/images/opcache/screenshot/files-v2.png) If you want to adjust the pagination length you can do so with the `per_page` configuration option.
### Reset cache ![Screenshot of the Cached files list showing filtered results and pagination](http://amnuts.com/images/opcache/screenshot/cached-v3.png)
You can reset the whole cache as well as force individual files to become invalidated so they will be cached again. ### Ignored files
Both reset types can be disabled with the options `allow_reset` and `allow_invalidate`. If you have set up a list of files which you don't want cache by supplying an `opcache.blacklist_filename` value, then the list of files will be listed within this tab.
If you have not supplied that configuration option in the `php.ini` file then this tab will not be displayed. If you set the `allow_filelist` configuration option to `false` then this tab will not be displayed irrespective of your ini setting.
### Preloaded files
PHP 7.4 introduced the ability to pre-load a set of files on server start by way of the `opcache.preload` setting in your `php.ini` file. If you have set that up then the list of files specifically pre-loaded will be listed within this tab.
As with the ignored file, if you have not supplied the ini setting, or the `allow_filelist` configuration option is `false`, then this tab will not be displayed.
### Reset the cache
You can reset the whole cache as well as force individual files, or groups of files, to become invalidated so that they will be cached again.
Resetting can be disabled with the use of the configuration options `allow_reset` and `allow_invalidate`.
### Real-time updates ### Real-time updates
The interface can poll every so often to get a fresh look at the opcache. You can change how often this happens with the option `refresh_time`. The React javascript library is used to handle data refresh so you don't need to keep reloading the page. The interface can poll every so often to get a fresh look at the opcache. You can change how often this happens with the configuration option `refresh_time`, which is in seconds.
## Project files When the real-time updates are active the interface will automatically update all the values as needed. Also, if you choose to invalidate any files or reset the cache it will do this without reloading the page, so the search term you've entered, or the page you've navigated to do not get reset. If the real-time update is not on then the page will reload on any invalidation usage.
The status.jsx file is provided solely for you to be able to edit the jsx code should you wish. For production purposes it's best to have the jsx code pre-compiled which is what's used in index.php. You do not need to use status.jsx at all in order to use the opcache gui. However, should you wish to compile the jsx code then you'll need to use [babel](https://babeljs.io/) or the [react-tools](https://www.npmjs.com/package/react-tools) (no longer supported with newer versions of React). For that, run `npm install` in the main directory. After that, run `node_modules/.bin/babel --presets @babel/preset-react src/status.jsx` to compile the file and paste it into the correct spot in `index.php`. MacOS users can pipe the output directly to `pbcopy` to copy the file to the clipboard. The same goes for linux users with `xclip`.
The composer.json file is provided to allow you to deploy the opcache gui a little easier by using composer.
## Releases ## Releases
**Version 3.0.0**\
Although the interface looks mostly the same, it's had a complete re-write under the hood! Some of the more notable changes are:
* New namespace for the base service class which ensure composer compatibility
* You can now paginate the cached files list to make it easier to render a large file list
* Any scripts that have been preloaded are displayed in a tab
* Any file paths ignored are displayed in a tab
* You can now invalidate all the files matching a search in one go
* jQuery has been removed; the whole interface is now using ReactJS and more modern javascript (so only modern browsers)
* The CSS is now using SASS and is now much easier to change all the colours of the interface as you wish
* SVGs are now used for any icons or gauge graphs
* A more responsive interface when the 'enable real-time' is activated
* Build script added to compile the ReactJS and SASS and put them into the single, simple, gui script
**Version 2.5.4**\ **Version 2.5.4**\
Refined placement of initial css namespace to play nicely within Moodle plugin and possibly other systems. Also tweaked some CSS. Refined placement of initial css namespace to play nicely within Moodle plugin and possibly other systems. Also tweaked some CSS.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,390 @@
$nav-header-color: #6CA6EF;
$nav-hover-color: #F4F4F4;
$nav-border-color: #CCC;
$nav-background-color: #FFF;
$nav-icon-color: #626262;
$nav-icon-active-color: #00ba00;
$table-header-color: #6CA6EF;
$table-row-color: #EFFEFF;
$table-row-color-alternative: #E0ECEF;
$table-row-border-color: #FFF;
$table-header-font-color: #FFF;
$table-header-border-color: #FFF;
$widget-header-color: #CDCDCD;
$widget-background-color: #EDEDED;
$widget-graph-fill-color: #6CA6EF;
$widget-graph-background-color: #E5E7E7E7;
$pagination-active-color: #4d75af;
$pagination-active-font-color: #FFF;
$pagination-hover-color: #FF7400;
$pagination-hover-font-color: #FFF;
$footer-border-color: #CCC;
@function toRGB ($color) {
@return "rgb(" + red($color) + ", " + green($color) + ", " + blue($color)+ ")";
}
:root {
--opcache-gui-graph-track-fill-color: #{$widget-graph-fill-color};
--opcache-gui-graph-track-background-color: #{$widget-graph-background-color};
}
.opcache-gui {
font-family: sans-serif;
font-size: 90%;
padding: 0;
margin: 0;
.hide {
display: none;
}
.sr-only {
border: 0 !important;
clip: rect(1px, 1px, 1px, 1px) !important;
-webkit-clip-path: inset(50%) !important;
clip-path: inset(50%) !important;
height: 1px !important;
margin: -1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important;
}
.main-nav {
padding-top: 20px;
}
.nav-tab-list {
list-style-type: none;
padding-left: 8px;
margin: 0;
border-bottom: 1px solid $nav-border-color;
}
.nav-tab {
display: inline-block;
margin: 0 0 -1px 0;
padding: 15px 30px;
border: 1px solid transparent;
border-bottom-color: $nav-border-color;
text-decoration: none;
background-color: $nav-background-color;
cursor: pointer;
user-select: none;
&:hover {
background-color: $nav-hover-color;
text-decoration: underline;
}
&.active {
border: 1px solid $nav-border-color;
border-bottom-color: $nav-background-color;
border-top: 3px solid $nav-header-color;
}
&.active:hover {
background-color: initial;
}
&:focus {
outline: 0;
text-decoration: underline;
}
}
.nav-tab-link-reset {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1.5em" height="1.5em" viewBox="0 0 24 24"><path d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" fill="#{toRGB($nav-icon-color)}"/></svg>');
&.is-resetting {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1.5em" height="1.5em" viewBox="0 0 24 24"><path d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" fill="#{toRGB($nav-icon-active-color)}"/></svg>');
}
}
.nav-tab-link-realtime {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1.5em" height="1.5em" viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8s-3.58 8-8 8z" fill="#{toRGB($nav-icon-color)}"/><path d="M12.5 7H11v6l5.25 3.15l.75-1.23l-4.5-2.67z" fill="#{toRGB($nav-icon-color)}"/></svg>');
&.live-update {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1.5em" height="1.5em" viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8s-3.58 8-8 8z" fill="#{toRGB($nav-icon-active-color)}"/><path d="M12.5 7H11v6l5.25 3.15l.75-1.23l-4.5-2.67z" fill="#{toRGB($nav-icon-active-color)}"/></svg>');
}
}
.nav-tab-link-reset, .nav-tab-link-realtime {
position: relative;
padding-left: 50px;
&.pulse::before {
content: "";
position: absolute;
top: 12px;
left: 25px;
width: 18px;
height: 18px;
z-index: 10;
opacity: 0;
background-color: transparent;
border: 2px solid $nav-icon-active-color;
border-radius: 100%;
animation: pulse 2s linear infinite;
}
}
.tab-content {
padding: 2em;
}
.tab-content-overview-counts {
width: 270px;
float: right;
}
.tab-content-overview-info {
margin-right: 280px;
}
.graph-widget {
max-width: 100%;
height: auto;
margin: 0 auto;
display: flex;
position: relative;
.widget-value {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
position: absolute;
top: 0;
width: 100%;
height: 100%;
margin: 0 auto;
font-size: 3.2em;
font-weight: 100;
color: $widget-graph-fill-color;
user-select: none;
}
}
.widget-panel {
background-color: $widget-background-color;
margin-bottom: 10px;
}
.widget-header {
background-color: $widget-header-color;
padding: 4px 6px;
margin: 0;
text-align: center;
font-size: 1rem;
font-weight: bold;
}
.widget-value {
margin: 0;
text-align: center;
span.large {
color: $widget-graph-fill-color;
font-size: 80pt;
margin: 0;
padding: 0;
text-align: center;
+ span {
font-size: 20pt;
margin: 0;
color: $widget-graph-fill-color;
}
}
}
.widget-info {
margin: 0;
padding: 10px;
* {
margin: 0;
line-height: 1.75em;
text-align: left;
}
}
.tables {
margin: 0 0 1em 0;
border-collapse: collapse;
width: 100%;
table-layout: fixed;
tr {
&:nth-child(odd) {
background-color: $table-row-color;
}
&:nth-child(even) {
background-color: $table-row-color-alternative;
}
}
th {
text-align: left;
padding: 6px;
background-color: $table-header-color;
color: $table-header-font-color;
border-color: $table-header-border-color;
font-weight: normal;
}
td {
padding: 4px 6px;
line-height: 1.4em;
vertical-align: top;
border-color: $table-row-border-color;
overflow: hidden;
overflow-wrap: break-word;
text-overflow: ellipsis;
}
}
.file-filter {
width: 520px;
}
.file-metainfo {
font-size: 80%;
&.invalid {
font-style: italic;
}
}
.file-pathname {
width: 70%;
display: block;
}
.nav-tab-link-reset,
.nav-tab-link-realtime,
.github-link {
background-repeat: no-repeat;
background-color: transparent;
}
.nav-tab-link-reset,
.nav-tab-link-realtime {
background-position: 24px 50%;
}
.github-link {
background-position: 5px 50%;
}
.main-footer {
border-top: 1px solid $footer-border-color;
padding: 1em 2em;
}
.github-link {
background-position: 0 50%;
padding: 2em 0 2em 2.3em;
text-decoration: none;
opacity: 0.7;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1.19em" height="1em" viewBox="0 0 1664 1408"><path d="M640 960q0 40-12.5 82t-43 76t-72.5 34t-72.5-34t-43-76t-12.5-82t12.5-82t43-76t72.5-34t72.5 34t43 76t12.5 82zm640 0q0 40-12.5 82t-43 76t-72.5 34t-72.5-34t-43-76t-12.5-82t12.5-82t43-76t72.5-34t72.5 34t43 76t12.5 82zm160 0q0-120-69-204t-187-84q-41 0-195 21q-71 11-157 11t-157-11q-152-21-195-21q-118 0-187 84t-69 204q0 88 32 153.5t81 103t122 60t140 29.5t149 7h168q82 0 149-7t140-29.5t122-60t81-103t32-153.5zm224-176q0 207-61 331q-38 77-105.5 133t-141 86t-170 47.5t-171.5 22t-167 4.5q-78 0-142-3t-147.5-12.5t-152.5-30t-137-51.5t-121-81t-86-115Q0 992 0 784q0-237 136-396q-27-82-27-170q0-116 51-218q108 0 190 39.5T539 163q147-35 309-35q148 0 280 32q105-82 187-121t189-39q51 102 51 218q0 87-27 168q136 160 136 398z" fill="#{toRGB($nav-icon-color)}"/></svg>');
font-size: 80%;
&:hover {
opacity: 1;
}
}
.file-cache-only {
margin-top: 0;
}
.pagination {
margin: 10px 0;
padding: 0;
li {
display: inline-block;
a {
display: inline-block;
display: inline-flex;
align-items: center;
white-space: nowrap;
line-height: 1;
padding: 0.5rem 0.75rem;
border-radius: 3px;
text-decoration: none;
height: 100%;
&.arrow {
font-size: 1.1rem;
}
&:active {
transform: translateY(2px);
}
&.active {
background-color: $pagination-active-color;
color: $pagination-active-font-color;
}
&:hover:not(.active) {
background-color: $pagination-hover-color;
color: $pagination-hover-font-color;
}
}
}
}
@media screen and (max-width: 750px) {
.nav-tab-list {
border-bottom: 0;
}
.nav-tab {
display: block;
margin: 0;
}
.nav-tab-link {
display: block;
margin: 0 10px;
padding: 10px 0 10px 30px;
border: 0;
}
.nav-tab-link[data-for].active {
border-bottom-color: $nav-border-color;
}
.tab-content-overview-info {
margin-right: auto;
clear: both;
}
.tab-content-overview-counts {
position: relative;
display: block;
width: 100%;
}
}
@media screen and (max-width: 550px) {
.file-filter {
width: 100%;
}
}
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50%,100% {
transform: scale(2);
opacity: 0;
}
}

27
build/build.php Normal file
View file

@ -0,0 +1,27 @@
<?php
$parentPath = dirname(__DIR__);
if (!file_exists($parentPath . '/node_modules')) {
echo "🐢 Installing node modules\n";
exec('npm install');
}
echo "🏗️ Building js and css\n";
chdir($parentPath);
exec('npm run compile-jsx');
exec('npm run compile-scss');
echo "🚀 Creating single build file\n";
$template = trim(file_get_contents(__DIR__ . '/template.phps'));
$jsOutput = trim(file_get_contents(__DIR__ . '/interface.js'));
$cssOutput = trim(file_get_contents(__DIR__ . '/interface.css'));
$phpOutput = trim(join('', array_slice(file($parentPath . '/src/Opcache/Service.php'), 3)));
$output = str_replace(
['{{JS_OUTPUT}}', '{{CSS_OUTPUT}}', '{{PHP_OUTPUT}}'],
[$jsOutput, $cssOutput, $phpOutput],
$template
);
file_put_contents($parentPath . '/index.php', $output);
echo "💯 Done!\n";

107
build/template.phps Normal file
View file

@ -0,0 +1,107 @@
<?php
namespace Amnuts\Opcache;
/**
* OPcache GUI
*
* A simple but effective single-file GUI for the OPcache PHP extension.
*
* @author Andrew Collington, andy@amnuts.com
* @version 3.0.0
* @link https://github.com/amnuts/opcache-gui
* @license MIT, http://acollington.mit-license.org/
*/
/*
* User configuration
* These are all the default values; you only really need to supply the ones
* that you wish to change.
*/
$options = [
'allow_filelist' => true, // show/hide the files tab
'allow_invalidate' => true, // give a link to invalidate files
'allow_reset' => true, // give option to reset the whole cache
'allow_realtime' => true, // give option to enable/disable real-time updates
'refresh_time' => 5, // how often the data will refresh, in seconds
'size_precision' => 2, // Digits after decimal point
'size_space' => false, // have '1MB' or '1 MB' when showing sizes
'charts' => true, // show gauge chart or just big numbers
'debounce_rate' => 250, // milliseconds after key press to send keyup event when filtering
'per_page' => 200, // How many results per page to show in the file list, false for no pagination
'cookie_name' => 'opcachegui', // name of cookie
'cookie_ttl' => 365, // days to store cookie
'highlight' => [
'memory' => true, // show the memory chart/big number
'hits' => true, // show the hit rate chart/big number
'keys' => true // show the keys used chart/big number
]
];
/*
* Shouldn't need to alter anything else below here
*/
if (!extension_loaded('Zend OPcache')) {
die('The Zend OPcache extension does not appear to be installed');
}
$ocEnabled = ini_get('opcache.enable');
if (empty($ocEnabled)) {
die('The Zend OPcache extension is installed but not active');
}
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
{{PHP_OUTPUT}}
$opcache = (new Service($options))->handle();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>OPcache statistics on <?= $opcache->getData('version', 'host'); ?></title>
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/axios/dist/axios.min.js" crossorigin></script>
<style type="text/css">
{{CSS_OUTPUT}}
</style>
</head>
<body style="padding: 0; margin: 0;">
<div class="opcache-gui" id="interface" />
<script type="text/javascript">
{{JS_OUTPUT}}
ReactDOM.render(React.createElement(Interface, {
allow: {
filelist: <?= $opcache->getOption('allow_filelist') ? 'true' : 'false'; ?>,
invalidate: <?= $opcache->getOption('allow_invalidate') ? 'true' : 'false'; ?>,
reset: <?= $opcache->getOption('allow_reset') ? 'true' : 'false'; ?>,
realtime: <?= $opcache->getOption('allow_realtime') ? 'true' : 'false'; ?>
},
cookie: {
name: '<?= $opcache->getOption('cookie_name'); ?>',
ttl: <?= $opcache->getOption('cookie_ttl'); ?>
},
opstate: <?= json_encode($opcache->getData()); ?>,
useCharts: <?= json_encode($opcache->getOption('charts')); ?>,
highlight: <?= json_encode($opcache->getOption('highlight')); ?>,
debounceRate: <?= $opcache->getOption('debounce_rate'); ?>,
perPageLimit: <?= json_encode($opcache->getOption('per_page')); ?>,
realtimeRefresh: <?= json_encode($opcache->getOption('refresh_time')); ?>
}), document.getElementById('interface'));
</script>
</body>
</html>

View file

@ -22,12 +22,14 @@
}, },
"require": { "require": {
"ext-Zend-OPcache": "*", "ext-Zend-OPcache": "*",
"php": ">=5.4" "php": ">=7.1.0"
}, },
"autoload": { "autoload": {
"psr-4" : { "psr-4" : {
"OpcacheGui\\" : "/" "Amnuts\\" : "src/"
} }
},
"scripts": {
"build": "php build/build.php"
} }
} }

2371
index.php

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,30 @@
{ {
"name": "opcache-gui",
"description": "A clean and responsive interface for Zend OPcache information, showing statistics, settings and cached files, and providing a real-time update for the information (using jQuery and React).",
"version": "3.0.0",
"main": "index.js",
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.0.0", "@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0", "@babel/core": "^7.0.0",
"@babel/preset-react": "^7.9.4" "@babel/preset-react": "^7.9.4",
"node-sass": "^4.14.1"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"compile-jsx": "node_modules/.bin/babel --presets @babel/preset-react build/_frontend/interface.jsx --plugins @babel/plugin-proposal-class-properties -o build/interface.js",
"compile-scss": "node_modules/.bin/node-sass -xi build/_frontend/interface.scss -o build/ --output-style=compressed"
},
"repository": {
"type": "git",
"url": "git+https://github.com/amnuts/opcache-gui.git"
},
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/amnuts/opcache-gui/issues"
},
"homepage": "https://github.com/amnuts/opcache-gui#readme",
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.10.4"
} }
} }

329
src/Opcache/Service.php Normal file
View file

@ -0,0 +1,329 @@
<?php
namespace Amnuts\Opcache;
class Service
{
const VERSION = '3.0.0';
protected $data;
protected $options;
protected $optimizationLevels;
protected $defaults = [
'allow_filelist' => true, // show/hide the files tab
'allow_invalidate' => true, // give a link to invalidate files
'allow_reset' => true, // give option to reset the whole cache
'allow_realtime' => true, // give option to enable/disable real-time updates
'refresh_time' => 5, // how often the data will refresh, in seconds
'size_precision' => 2, // Digits after decimal point
'size_space' => false, // have '1MB' or '1 MB' when showing sizes
'charts' => true, // show gauge chart or just big numbers
'debounce_rate' => 250, // milliseconds after key press to send keyup event when filtering
'per_page' => 200, // How many results per page to show in the file list, false for no pagination
'cookie_name' => 'opcachegui', // name of cookie
'cookie_ttl' => 365, // days to store cookie
'highlight' => [
'memory' => true, // show the memory chart/big number
'hits' => true, // show the hit rate chart/big number
'keys' => true // show the keys used chart/big number
]
];
/**
* Service constructor.
* @param array $options
*/
public function __construct(array $options = [])
{
$this->optimizationLevels = [
1 << 0 => 'CSE, STRING construction',
1 << 1 => 'Constant conversion and jumps',
1 << 2 => '++, +=, series of jumps',
1 << 3 => 'INIT_FCALL_BY_NAME -> DO_FCALL',
1 << 4 => 'CFG based optimization',
1 << 5 => 'DFA based optimization',
1 << 6 => 'CALL GRAPH optimization',
1 << 7 => 'SCCP (constant propagation)',
1 << 8 => 'TMP VAR usage',
1 << 9 => 'NOP removal',
1 << 10 => 'Merge equal constants',
1 << 11 => 'Adjust used stack',
1 << 12 => 'Remove unused variables',
1 << 13 => 'DCE (dead code elimination)',
1 << 14 => '(unsafe) Collect constants',
1 << 15 => 'Inline functions'
];
$this->options = array_merge($this->defaults, $options);
$this->data = $this->compileState();
}
/**
* @return $this
*/
public function handle(): Service
{
$response = function($success) {
if ($this->isJsonRequest()) {
echo '{ "success": "' . ($success ? 'yes' : 'no') . '" }';
} else {
header('Location: ?');
}
exit;
};
if (isset($_GET['reset']) && $this->getOption('allow_reset')) {
$response($this->resetCache());
} else if (isset($_GET['invalidate']) && $this->getOption('allow_invalidate')) {
$response($this->resetCache($_GET['invalidate']));
} else if (isset($_GET['invalidate_searched']) && $this->getOption('allow_invalidate')) {
$response($this->resetSearched($_GET['invalidate_searched']));
} else if (isset($_GET['invalidate_searched']) && $this->getOption('allow_invalidate')) {
$response($this->resetSearched($_GET['invalidate_searched']));
} else if ($this->isJsonRequest() && $this->getOption('allow_realtime')) {
echo json_encode($this->getData((empty($_GET['section']) ? null : $_GET['section'])));
exit;
}
return $this;
}
/**
* @param string|null $name
* @return array|mixed|null
*/
public function getOption(?string $name = null)
{
if ($name === null) {
return $this->options;
}
return (isset($this->options[$name])
? $this->options[$name]
: null
);
}
/**
* @param string|null $section
* @param string|null $property
* @return array|mixed|null
*/
public function getData(?string $section = null, ?string $property = null)
{
if ($section === null) {
return $this->data;
}
$section = strtolower($section);
if (isset($this->data[$section])) {
if ($property === null || !isset($this->data[$section][$property])) {
return $this->data[$section];
}
return $this->data[$section][$property];
}
return null;
}
/**
* @return bool
*/
public function canInvalidate(): bool
{
return ($this->getOption('allow_invalidate') && function_exists('opcache_invalidate'));
}
/**
* @param string|null $file
* @return bool
*/
public function resetCache(?string $file = null): bool
{
$success = false;
if ($file === null) {
$success = opcache_reset();
} else if (function_exists('opcache_invalidate')) {
$success = opcache_invalidate(urldecode($file), true);
}
if ($success) {
$this->compileState();
}
return $success;
}
/**
* @param string $search
* @return bool
*/
public function resetSearched(string $search): bool
{
$found = $success = 0;
$search = urldecode($search);
foreach ($this->getData('files') as $file) {
if (strpos($file['full_path'], $search) !== false) {
++$found;
$success += (int)opcache_invalidate($file['full_path'], true);
}
}
if ($success) {
$this->compileState();
}
return $found === $success;
}
/**
* @param mixed $size
* @return string
*/
protected function size($size): string
{
$i = 0;
$val = ['b', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
while (($size / 1024) > 1) {
$size /= 1024;
++$i;
}
return sprintf('%.' . $this->getOption('size_precision') . 'f%s%s',
$size, ($this->getOption('size_space') ? ' ' : ''), $val[$i]
);
}
/**
* @return bool
*/
protected function isJsonRequest(): bool
{
return !empty($_SERVER['HTTP_ACCEPT'])
&& stripos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false;
}
/**
* @return array
*/
protected function compileState(): array
{
$status = opcache_get_status();
$config = opcache_get_configuration();
$missingConfig = array_diff_key(ini_get_all('zend opcache', false), $config['directives']);
if (!empty($missingConfig)) {
$config['directives'] = array_merge($config['directives'], $missingConfig);
}
$files = [];
if (!empty($status['scripts']) && $this->getOption('allow_filelist')) {
uasort($status['scripts'], function ($a, $b) {
return $a['hits'] < $b['hits'];
});
foreach ($status['scripts'] as &$file) {
$file['full_path'] = str_replace('\\', '/', $file['full_path']);
$file['readable'] = [
'hits' => number_format($file['hits']),
'memory_consumption' => $this->size($file['memory_consumption'])
];
}
$files = array_values($status['scripts']);
}
if ($config['directives']['opcache.file_cache_only'] || !empty($status['file_cache_only'])) {
$overview = false;
} else {
$overview = array_merge(
$status['memory_usage'], $status['opcache_statistics'], [
'used_memory_percentage' => round(100 * (
($status['memory_usage']['used_memory'] + $status['memory_usage']['wasted_memory'])
/ $config['directives']['opcache.memory_consumption']
)),
'hit_rate_percentage' => round($status['opcache_statistics']['opcache_hit_rate']),
'used_key_percentage' => round(100 * (
$status['opcache_statistics']['num_cached_keys']
/ $status['opcache_statistics']['max_cached_keys']
)),
'wasted_percentage' => round($status['memory_usage']['current_wasted_percentage'], 2),
'readable' => [
'total_memory' => $this->size($config['directives']['opcache.memory_consumption']),
'used_memory' => $this->size($status['memory_usage']['used_memory']),
'free_memory' => $this->size($status['memory_usage']['free_memory']),
'wasted_memory' => $this->size($status['memory_usage']['wasted_memory']),
'num_cached_scripts' => number_format($status['opcache_statistics']['num_cached_scripts']),
'hits' => number_format($status['opcache_statistics']['hits']),
'misses' => number_format($status['opcache_statistics']['misses']),
'blacklist_miss' => number_format($status['opcache_statistics']['blacklist_misses']),
'num_cached_keys' => number_format($status['opcache_statistics']['num_cached_keys']),
'max_cached_keys' => number_format($status['opcache_statistics']['max_cached_keys']),
'interned' => null,
'start_time' => date('Y-m-d H:i:s', $status['opcache_statistics']['start_time']),
'last_restart_time' => ($status['opcache_statistics']['last_restart_time'] == 0
? 'never'
: date('Y-m-d H:i:s', $status['opcache_statistics']['last_restart_time'])
)
]
]
);
}
$preload = [];
if (!empty($status['preload_statistics']['scripts']) && $this->getOption('allow_filelist')) {
$preload = $status['preload_statistics']['scripts'];
sort($preload, SORT_STRING);
if ($overview) {
$overview['preload_memory'] = $status['preload_statistics']['memory_consumption'];
$overview['readable']['preload_memory'] = $this->size($status['preload_statistics']['memory_consumption']);
}
}
if (!empty($status['interned_strings_usage'])) {
$overview['readable']['interned'] = [
'buffer_size' => $this->size($status['interned_strings_usage']['buffer_size']),
'strings_used_memory' => $this->size($status['interned_strings_usage']['used_memory']),
'strings_free_memory' => $this->size($status['interned_strings_usage']['free_memory']),
'number_of_strings' => number_format($status['interned_strings_usage']['number_of_strings'])
];
}
$directives = [];
ksort($config['directives']);
foreach ($config['directives'] as $k => $v) {
if (in_array($k, ['opcache.max_file_size', 'opcache.memory_consumption']) && $v) {
$v = $this->size($v) . " ({$v})";
} elseif ($k == 'opcache.optimization_level') {
$levels = [];
foreach ($this->optimizationLevels as $level => $info) {
if ($level & $v) {
$levels[] = $info;
}
}
$v = $levels ?: 'none';
}
$directives[] = [
'k' => $k,
'v' => $v
];
}
$version = array_merge(
$config['version'],
[
'php' => phpversion(),
'server' => $_SERVER['SERVER_SOFTWARE'] ?: '',
'host' => (function_exists('gethostname')
? gethostname()
: (php_uname('n')
?: (empty($_SERVER['SERVER_NAME'])
? $_SERVER['HOST_NAME']
: $_SERVER['SERVER_NAME']
)
)
),
'gui' => self::VERSION
]
);
return [
'version' => $version,
'overview' => $overview,
'files' => $files,
'preload' => $preload,
'directives' => $directives,
'blacklist' => $config['blacklist'],
'functions' => get_extension_funcs('Zend OPcache')
];
}
}

View file

@ -1,316 +0,0 @@
var UsageGraph = React.createClass({
getInitialState: function() {
return {
gauge : null
};
},
componentDidMount: function() {
if (this.props.chart) {
this.state.gauge = new Gauge('#' + this.props.gaugeId);
this.state.gauge.setValue(this.props.value);
}
},
componentDidUpdate: function() {
if (this.state.gauge != null) {
this.state.gauge.setValue(this.props.value);
}
},
render: function() {
if (this.props.chart == true) {
return(<canvas id={this.props.gaugeId} className="graph-widget" width="250" height="250" data-value={this.props.value} />);
}
return(<p><span className="large">{this.props.value}</span><span>%</span></p>);
}
});
var MemoryUsagePanel = React.createClass({
render: function() {
return (
<div className="widget-panel">
<h3 className="widget-header">memory usage</h3>
<div className="widget-value widget-info">
<p><b>total memory:</b> {this.props.total}</p>
<p><b>used memory:</b> {this.props.used}</p>
<p><b>free memory:</b> {this.props.free}</p>
<p><b>wasted memory:</b> {this.props.wasted} ({this.props.wastedPercent}%)</p>
</div>
</div>
);
}
});
var StatisticsPanel = React.createClass({
render: function() {
return (
<div className="widget-panel">
<h3 className="widget-header">opcache statistics</h3>
<div className="widget-value widget-info">
<p><b>number of cached files:</b> {this.props.num_cached_scripts}</p>
<p><b>number of hits:</b> {this.props.hits}</p>
<p><b>number of misses:</b> {this.props.misses}</p>
<p><b>blacklist misses:</b> {this.props.blacklist_miss}</p>
<p><b>number of cached keys:</b> {this.props.num_cached_keys}</p>
<p><b>max cached keys:</b> {this.props.max_cached_keys}</p>
</div>
</div>
);
}
});
var InternedStringsPanel = React.createClass({
render: function() {
return (
<div className="widget-panel">
<h3 className="widget-header">interned strings usage</h3>
<div className="widget-value widget-info">
<p><b>buffer size:</b> {this.props.buffer_size}</p>
<p><b>used memory:</b> {this.props.strings_used_memory}</p>
<p><b>free memory:</b> {this.props.strings_free_memory}</p>
<p><b>number of strings:</b> {this.props.number_of_strings}</p>
</div>
</div>
);
}
});
var OverviewCounts = React.createClass({
getInitialState: function() {
return {
data : opstate.overview,
chart : useCharts,
highlight: highlight
};
},
render: function() {
if (this.state.data == false) {
return (
<p class="file-cache-only">
You have <i>opcache.file_cache_only</i> turned on. As a result, the memory information is not available. Statistics and file list may also not be returned by <i>opcache_get_statistics()</i>.
</p>
);
}
var interned = (this.state.data.readable.interned != null
? <InternedStringsPanel
buffer_size={this.state.data.readable.interned.buffer_size}
strings_used_memory={this.state.data.readable.interned.strings_used_memory}
strings_free_memory={this.state.data.readable.interned.strings_free_memory}
number_of_strings={this.state.data.readable.interned.number_of_strings}
/>
: ''
);
var memoryHighlight = this.state.highlight.memory ? (
<div className="widget-panel">
<h3 className="widget-header">memory</h3>
<p className="widget-value"><UsageGraph chart={this.state.chart} value={this.state.data.used_memory_percentage} gaugeId="memoryUsageCanvas"/></p>
</div>
) : null;
var hitsHighlight = this.state.highlight.hits ? (
<div className="widget-panel">
<h3 className="widget-header">hit rate</h3>
<p className="widget-value"><UsageGraph chart={this.state.chart} value={this.state.data.hit_rate_percentage} gaugeId="hitRateCanvas"/></p>
</div>
) : null;
var keysHighlight = this.state.highlight.keys ? (
<div className="widget-panel">
<h3 className="widget-header">keys</h3>
<p className="widget-value"><UsageGraph chart={this.state.chart} value={this.state.data.used_key_percentage} gaugeId="keyUsageCanvas"/></p>
</div>
) : null;
return (
<div>
{memoryHighlight}
{hitsHighlight}
{keysHighlight}
<MemoryUsagePanel
total={this.state.data.readable.total_memory}
used={this.state.data.readable.used_memory}
free={this.state.data.readable.free_memory}
wasted={this.state.data.readable.wasted_memory}
wastedPercent={this.state.data.wasted_percentage}
/>
<StatisticsPanel
num_cached_scripts={this.state.data.readable.num_cached_scripts}
hits={this.state.data.readable.hits}
misses={this.state.data.readable.misses}
blacklist_miss={this.state.data.readable.blacklist_miss}
num_cached_keys={this.state.data.readable.num_cached_keys}
max_cached_keys={this.state.data.readable.max_cached_keys}
/>
{interned}
</div>
);
}
});
var GeneralInfo = React.createClass({
getInitialState: function() {
return {
version : opstate.version,
start : opstate.overview ? opstate.overview.readable.start_time : null,
reset : opstate.overview ? opstate.overview.readable.last_restart_time : null
};
},
render: function() {
var startTime = this.state.start
? <tr><td>Start time</td><td>{this.state.start}</td></tr>
: '';
var lastReset = this.state.reset
? <tr><td>Last reset</td><td>{this.state.reset}</td></tr>
: '';
return (
<table className="tables general-info-table">
<thead>
<tr><th colSpan="2">General info</th></tr>
</thead>
<tbody>
<tr><td>Zend OPcache</td><td>{this.state.version.version}</td></tr>
<tr><td>PHP</td><td>{this.state.version.php}</td></tr>
<tr><td>Host</td><td>{this.state.version.host}</td></tr>
<tr><td>Server Software</td><td>{this.state.version.server}</td></tr>
{ startTime }
{ lastReset }
</tbody>
</table>
);
}
});
var Directives = React.createClass({
getInitialState: function() {
return { data : opstate.directives };
},
render: function() {
var directiveNodes = this.state.data.map(function(directive) {
var map = { 'opcache.':'', '_':' ' };
var dShow = directive.k.replace(/opcache\.|_/gi, function(matched){
return map[matched];
});
var vShow;
if (directive.v === true || directive.v === false) {
vShow = React.createElement('i', {}, directive.v.toString());
} else if (directive.v === '') {
vShow = React.createElement('i', {}, 'no value');
} else {
if (Array.isArray(directive.v)) {
vShow = directive.v.map((item, key) => {
return <span key={key}>{item}<br/></span>
});
} else {
vShow = directive.v;
}
}
return (
<tr key={directive.k}>
<td title={'View ' + directive.k + ' manual entry'}><a href={'http://php.net/manual/en/opcache.configuration.php#ini.'
+ (directive.k).replace(/_/g,'-')} target="_blank">{dShow}</a></td>
<td>{vShow}</td>
</tr>
);
});
return (
<table className="tables directives-table">
<thead>
<tr><th colSpan="2">Directives</th></tr>
</thead>
<tbody>{directiveNodes}</tbody>
</table>
);
}
});
var Files = React.createClass({
getInitialState: function() {
return {
data : opstate.files,
showing: null,
allowFiles: allowFiles
};
},
handleInvalidate: function(e) {
e.preventDefault();
if (realtime) {
$.get('#', { invalidate: e.currentTarget.getAttribute('data-file') }, function(data) {
console.log('success: ' + data.success);
}, 'json');
} else {
window.location.href = e.currentTarget.href;
}
},
render: function() {
if (this.state.allowFiles) {
var fileNodes = this.state.data.map(function(file, i) {
var invalidate, invalidated;
if (file.timestamp == 0) {
invalidated = <span><i className="invalid metainfo"> - has been invalidated</i></span>;
}
if (canInvalidate) {
invalidate = <span>,&nbsp;<a className="file-metainfo" href={'?invalidate='
+ file.full_path} data-file={file.full_path} onClick={this.handleInvalidate}>force file invalidation</a></span>;
}
return (
<tr key={file.full_path} data-path={file.full_path.toLowerCase()} className={i%2?'alternate':''}>
<td>
<span className="file-pathname">{file.full_path}</span>
<FilesMeta data={[file.readable.hits, file.readable.memory_consumption, file.last_used]} />
{invalidate}
{invalidated}
</td>
</tr>
);
}.bind(this));
return (
<div>
<FilesListed showing={this.state.showing}/>
<table className="tables file-list-table">
<thead>
<tr>
<th>Script</th>
</tr>
</thead>
<tbody>{fileNodes}</tbody>
</table>
</div>
);
} else {
return <span></span>;
}
}
});
var FilesMeta = React.createClass({
render: function() {
return (
<span className="file-metainfo">
<b>hits: </b><span>{this.props.data[0]}, </span>
<b>memory: </b><span>{this.props.data[1]}, </span>
<b>last used: </b><span>{this.props.data[2]}</span>
</span>
);
}
});
var FilesListed = React.createClass({
getInitialState: function() {
return {
formatted : opstate.overview ? opstate.overview.readable.num_cached_scripts : 0,
total : opstate.overview ? opstate.overview.num_cached_scripts : 0
};
},
render: function() {
var display = this.state.formatted + ' file' + (this.state.total == 1 ? '' : 's') + ' cached';
if (this.props.showing !== null && this.props.showing != this.state.total) {
display += ', ' + this.props.showing + ' showing due to filter';
}
return (<h3>{display}</h3>);
}
});
var overviewCountsObj = ReactDOM.render(<OverviewCounts/>, document.getElementById('counts'));
var generalInfoObj = ReactDOM.render(<GeneralInfo/>, document.getElementById('generalInfo'));
var filesObj = ReactDOM.render(<Files/>, document.getElementById('filelist'));
ReactDOM.render(<Directives/>, document.getElementById('directives'));