interface.jsx 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165
  1. class Interface extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. realtime: this.getCookie(),
  6. resetting: false,
  7. opstate: props.opstate
  8. }
  9. this.polling = false;
  10. this.isSecure = (window.location.protocol === 'https:');
  11. if (this.getCookie()) {
  12. this.startTimer();
  13. }
  14. }
  15. startTimer = () => {
  16. this.setState({realtime: true})
  17. this.polling = setInterval(() => {
  18. this.setState({fetching: true, resetting: false});
  19. axios.get(window.location.pathname, {time: Date.now()})
  20. .then((response) => {
  21. this.setState({opstate: response.data});
  22. });
  23. }, this.props.realtimeRefresh * 1000);
  24. }
  25. stopTimer = () => {
  26. this.setState({realtime: false, resetting: false})
  27. clearInterval(this.polling)
  28. }
  29. realtimeHandler = () => {
  30. const realtime = !this.state.realtime;
  31. if (!realtime) {
  32. this.stopTimer();
  33. this.removeCookie();
  34. } else {
  35. this.startTimer();
  36. this.setCookie();
  37. }
  38. }
  39. resetHandler = () => {
  40. if (this.state.realtime) {
  41. this.setState({resetting: true});
  42. axios.get(window.location.pathname, {params: {reset: 1}})
  43. .then((response) => {
  44. console.log('success: ', response.data);
  45. });
  46. } else {
  47. window.location.href = '?reset=1';
  48. }
  49. }
  50. setCookie = () => {
  51. let d = new Date();
  52. d.setTime(d.getTime() + (this.props.cookie.ttl * 86400000));
  53. document.cookie = `${this.props.cookie.name}=true;expires=${d.toUTCString()};path=/${this.isSecure ? ';secure' : ''}`;
  54. }
  55. removeCookie = () => {
  56. document.cookie = `${this.props.cookie.name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/${this.isSecure ? ';secure' : ''}`;
  57. }
  58. getCookie = () => {
  59. const v = document.cookie.match(`(^|;) ?${this.props.cookie.name}=([^;]*)(;|$)`);
  60. return v ? !!v[2] : false;
  61. };
  62. render() {
  63. const { opstate, realtimeRefresh, ...otherProps } = this.props;
  64. return (
  65. <>
  66. <header>
  67. <MainNavigation {...otherProps}
  68. opstate={this.state.opstate}
  69. realtime={this.state.realtime}
  70. resetting={this.state.resetting}
  71. realtimeHandler={this.realtimeHandler}
  72. resetHandler={this.resetHandler}
  73. />
  74. </header>
  75. <Footer version={this.props.opstate.version.gui} />
  76. </>
  77. );
  78. }
  79. }
  80. function MainNavigation(props) {
  81. return (
  82. <nav className="main-nav">
  83. <Tabs>
  84. <div label="Overview" tabId="overview" tabIndex={1}>
  85. <OverviewCounts
  86. overview={props.opstate.overview}
  87. highlight={props.highlight}
  88. useCharts={props.useCharts}
  89. />
  90. <div id="info" className="tab-content-overview-info">
  91. <GeneralInfo
  92. start={props.opstate.overview && props.opstate.overview.readable.start_time || null}
  93. reset={props.opstate.overview && props.opstate.overview.readable.last_restart_time || null}
  94. version={props.opstate.version}
  95. />
  96. <Directives
  97. directives={props.opstate.directives}
  98. />
  99. <Functions
  100. functions={props.opstate.functions}
  101. />
  102. </div>
  103. </div>
  104. {
  105. props.allow.filelist &&
  106. <div label="Cached" tabId="cached" tabIndex={2}>
  107. <CachedFiles
  108. perPageLimit={props.perPageLimit}
  109. allFiles={props.opstate.files}
  110. searchTerm={props.searchTerm}
  111. debounceRate={props.debounceRate}
  112. allow={{fileList: props.allow.filelist, invalidate: props.allow.invalidate}}
  113. realtime={props.realtime}
  114. />
  115. </div>
  116. }
  117. {
  118. (props.allow.filelist && props.opstate.blacklist.length &&
  119. <div label="Ignored" tabId="ignored" tabIndex={3}>
  120. <IgnoredFiles
  121. perPageLimit={props.perPageLimit}
  122. allFiles={props.opstate.blacklist}
  123. allow={{fileList: props.allow.filelist }}
  124. />
  125. </div>)
  126. }
  127. {
  128. (props.allow.filelist && props.opstate.preload.length &&
  129. <div label="Preloaded" tabId="preloaded" tabIndex={4}>
  130. <PreloadedFiles
  131. perPageLimit={props.perPageLimit}
  132. allFiles={props.opstate.preload}
  133. allow={{fileList: props.allow.filelist }}
  134. />
  135. </div>)
  136. }
  137. {
  138. props.allow.reset &&
  139. <div label="Reset cache" tabId="resetCache"
  140. className={`nav-tab-link-reset${props.resetting ? ' is-resetting pulse' : ''}`}
  141. handler={props.resetHandler}
  142. tabIndex={5}
  143. ></div>
  144. }
  145. {
  146. props.allow.realtime &&
  147. <div label={`${props.realtime ? 'Disable' : 'Enable'} real-time update`} tabId="toggleRealtime"
  148. className={`nav-tab-link-realtime${props.realtime ? ' live-update pulse' : ''}`}
  149. handler={props.realtimeHandler}
  150. tabIndex={6}
  151. ></div>
  152. }
  153. </Tabs>
  154. </nav>
  155. );
  156. }
  157. class Tabs extends React.Component {
  158. constructor(props) {
  159. super(props);
  160. this.state = {
  161. activeTab: this.props.children[0].props.label,
  162. };
  163. }
  164. onClickTabItem = (tab) => {
  165. this.setState({ activeTab: tab });
  166. }
  167. render() {
  168. const {
  169. onClickTabItem,
  170. state: { activeTab }
  171. } = this;
  172. const children = this.props.children.filter(Boolean);
  173. return (
  174. <>
  175. <ul className="nav-tab-list">
  176. {children.map((child) => {
  177. const { tabId, label, className, handler, tabIndex } = child.props;
  178. return (
  179. <Tab
  180. activeTab={activeTab}
  181. key={tabId}
  182. label={label}
  183. onClick={handler || onClickTabItem}
  184. className={className}
  185. tabIndex={tabIndex}
  186. tabId={tabId}
  187. />
  188. );
  189. })}
  190. </ul>
  191. <div className="tab-content">
  192. {children.map((child) => (
  193. <div key={child.props.label}
  194. style={{ display: child.props.label === activeTab ? 'block' : 'none' }}
  195. id={`${child.props.tabId}-content`}
  196. >
  197. {child.props.children}
  198. </div>
  199. ))}
  200. </div>
  201. </>
  202. );
  203. }
  204. }
  205. class Tab extends React.Component {
  206. onClick = () => {
  207. const { label, onClick } = this.props;
  208. onClick(label);
  209. }
  210. render() {
  211. const {
  212. onClick,
  213. props: { activeTab, label, tabIndex, tabId },
  214. } = this;
  215. let className = 'nav-tab';
  216. if (this.props.className) {
  217. className += ` ${this.props.className}`;
  218. }
  219. if (activeTab === label) {
  220. className += ' active';
  221. }
  222. return (
  223. <li className={className}
  224. onClick={onClick}
  225. tabIndex={tabIndex}
  226. role="tab"
  227. aria-controls={`${tabId}-content`}
  228. >{label}</li>
  229. );
  230. }
  231. }
  232. function OverviewCounts(props) {
  233. if (props.overview === false) {
  234. return (
  235. <p class="file-cache-only">
  236. 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>.
  237. </p>
  238. );
  239. }
  240. const graphList = [
  241. {id: 'memoryUsageCanvas', title: 'memory', show: props.highlight.memory, value: props.overview.used_memory_percentage},
  242. {id: 'hitRateCanvas', title: 'hit rate', show: props.highlight.hits, value: props.overview.hit_rate_percentage},
  243. {id: 'keyUsageCanvas', title: 'keys', show: props.highlight.keys, value: props.overview.used_key_percentage},
  244. {id: 'jitUsageCanvas', title: 'jit buffer', show: props.highlight.jit, value: props.overview.jit_buffer_used_percentage}
  245. ];
  246. return (
  247. <div id="counts" className="tab-content-overview-counts">
  248. {graphList.map((graph) => {
  249. if (!graph.show) {
  250. return null;
  251. }
  252. return (
  253. <div className="widget-panel" key={graph.id}>
  254. <h3 className="widget-header">{graph.title}</h3>
  255. <UsageGraph charts={props.useCharts} value={graph.value} gaugeId={graph.id} />
  256. </div>
  257. );
  258. })}
  259. <MemoryUsagePanel
  260. total={props.overview.readable.total_memory}
  261. used={props.overview.readable.used_memory}
  262. free={props.overview.readable.free_memory}
  263. wasted={props.overview.readable.wasted_memory}
  264. preload={props.overview.readable.preload_memory || null}
  265. wastedPercent={props.overview.wasted_percentage}
  266. jitBuffer={props.overview.readable.jit_buffer_size || null}
  267. jitBufferFree={props.overview.readable.jit_buffer_free || null}
  268. jitBufferFreePercentage={props.overview.jit_buffer_used_percentage || null}
  269. />
  270. <StatisticsPanel
  271. num_cached_scripts={props.overview.readable.num_cached_scripts}
  272. hits={props.overview.readable.hits}
  273. misses={props.overview.readable.misses}
  274. blacklist_miss={props.overview.readable.blacklist_miss}
  275. num_cached_keys={props.overview.readable.num_cached_keys}
  276. max_cached_keys={props.overview.readable.max_cached_keys}
  277. />
  278. {props.overview.readable.interned &&
  279. <InternedStringsPanel
  280. buffer_size={props.overview.readable.interned.buffer_size}
  281. strings_used_memory={props.overview.readable.interned.strings_used_memory}
  282. strings_free_memory={props.overview.readable.interned.strings_free_memory}
  283. number_of_strings={props.overview.readable.interned.number_of_strings}
  284. />
  285. }
  286. </div>
  287. );
  288. }
  289. function GeneralInfo(props) {
  290. return (
  291. <table className="tables general-info-table">
  292. <thead>
  293. <tr><th colSpan="2">General info</th></tr>
  294. </thead>
  295. <tbody>
  296. <tr><td>Zend OPcache</td><td>{props.version.version}</td></tr>
  297. <tr><td>PHP</td><td>{props.version.php}</td></tr>
  298. <tr><td>Host</td><td>{props.version.host}</td></tr>
  299. <tr><td>Server Software</td><td>{props.version.server}</td></tr>
  300. { props.start ? <tr><td>Start time</td><td>{props.start}</td></tr> : null }
  301. { props.reset ? <tr><td>Last reset</td><td>{props.reset}</td></tr> : null }
  302. </tbody>
  303. </table>
  304. );
  305. }
  306. function Directives(props) {
  307. let directiveList = (directive) => {
  308. return (
  309. <ul className="directive-list">{
  310. directive.v.map((item, key) => {
  311. return Array.isArray(item)
  312. ? <li key={"sublist_" + key}>{directiveList({v:item})}</li>
  313. : <li key={key}>{item}</li>
  314. })
  315. }</ul>
  316. );
  317. };
  318. let directiveNodes = props.directives.map(function(directive) {
  319. let map = { 'opcache.':'', '_':' ' };
  320. let dShow = directive.k.replace(/opcache\.|_/gi, function(matched){
  321. return map[matched];
  322. });
  323. let vShow;
  324. if (directive.v === true || directive.v === false) {
  325. vShow = React.createElement('i', {}, directive.v.toString());
  326. } else if (directive.v === '') {
  327. vShow = React.createElement('i', {}, 'no value');
  328. } else {
  329. if (Array.isArray(directive.v)) {
  330. vShow = directiveList(directive);
  331. } else {
  332. vShow = directive.v;
  333. }
  334. }
  335. return (
  336. <tr key={directive.k}>
  337. <td title={'View ' + directive.k + ' manual entry'}><a href={'https://php.net/manual/en/opcache.configuration.php#ini.'
  338. + (directive.k).replace(/_/g,'-')} target="_blank">{dShow}</a></td>
  339. <td>{vShow}</td>
  340. </tr>
  341. );
  342. });
  343. return (
  344. <table className="tables directives-table">
  345. <thead><tr><th colSpan="2">Directives</th></tr></thead>
  346. <tbody>{directiveNodes}</tbody>
  347. </table>
  348. );
  349. }
  350. function Functions(props) {
  351. return (
  352. <div id="functions">
  353. <table className="tables">
  354. <thead><tr><th>Available functions</th></tr></thead>
  355. <tbody>
  356. {props.functions.map(f =>
  357. <tr key={f}><td><a href={"https://php.net/"+f} title="View manual page" target="_blank">{f}</a></td></tr>
  358. )}
  359. </tbody>
  360. </table>
  361. </div>
  362. );
  363. }
  364. function UsageGraph(props) {
  365. const percentage = Math.round(((3.6 * props.value)/360)*100);
  366. return (props.charts
  367. ? <ReactCustomizableProgressbar
  368. progress={percentage}
  369. radius={100}
  370. strokeWidth={30}
  371. trackStrokeWidth={30}
  372. strokeColor={getComputedStyle(document.documentElement).getPropertyValue('--opcache-gui-graph-track-fill-color') || "#6CA6EF"}
  373. trackStrokeColor={getComputedStyle(document.documentElement).getPropertyValue('--opcache-gui-graph-track-background-color') || "#CCC"}
  374. gaugeId={props.gaugeId}
  375. />
  376. : <p className="widget-value"><span className="large">{percentage}</span><span>%</span></p>
  377. );
  378. }
  379. /**
  380. * This component is from <https://github.com/martyan/react-customizable-progressbar/>
  381. * MIT License (MIT), Copyright (c) 2019 Martin Juzl
  382. */
  383. class ReactCustomizableProgressbar extends React.Component {
  384. constructor(props) {
  385. super(props);
  386. this.state = {
  387. animationInited: false
  388. };
  389. }
  390. componentDidMount() {
  391. const { initialAnimation, initialAnimationDelay } = this.props
  392. if (initialAnimation)
  393. setTimeout(this.initAnimation, initialAnimationDelay)
  394. }
  395. initAnimation = () => {
  396. this.setState({ animationInited: true })
  397. }
  398. getProgress = () => {
  399. const { initialAnimation, progress } = this.props
  400. const { animationInited } = this.state
  401. return initialAnimation && !animationInited ? 0 : progress
  402. }
  403. getStrokeDashoffset = strokeLength => {
  404. const { counterClockwise, inverse, steps } = this.props
  405. const progress = this.getProgress()
  406. const progressLength = (strokeLength / steps) * (steps - progress)
  407. if (inverse) return counterClockwise ? 0 : progressLength - strokeLength
  408. return counterClockwise ? -1 * progressLength : progressLength
  409. }
  410. getStrokeDashArray = (strokeLength, circumference) => {
  411. const { counterClockwise, inverse, steps } = this.props
  412. const progress = this.getProgress()
  413. const progressLength = (strokeLength / steps) * (steps - progress)
  414. if (inverse) return `${progressLength}, ${circumference}`
  415. return counterClockwise
  416. ? `${strokeLength * (progress / 100)}, ${circumference}`
  417. : `${strokeLength}, ${circumference}`
  418. }
  419. getTrackStrokeDashArray = (strokeLength, circumference) => {
  420. const { initialAnimation } = this.props
  421. const { animationInited } = this.state
  422. if (initialAnimation && !animationInited) return `0, ${circumference}`
  423. return `${strokeLength}, ${circumference}`
  424. }
  425. getExtendedWidth = () => {
  426. const {
  427. strokeWidth,
  428. pointerRadius,
  429. pointerStrokeWidth,
  430. trackStrokeWidth
  431. } = this.props
  432. const pointerWidth = pointerRadius + pointerStrokeWidth
  433. if (pointerWidth > strokeWidth && pointerWidth > trackStrokeWidth) return pointerWidth * 2
  434. else if (strokeWidth > trackStrokeWidth) return strokeWidth * 2
  435. else return trackStrokeWidth * 2
  436. }
  437. getPointerAngle = () => {
  438. const { cut, counterClockwise, steps } = this.props
  439. const progress = this.getProgress()
  440. return counterClockwise
  441. ? ((360 - cut) / steps) * (steps - progress)
  442. : ((360 - cut) / steps) * progress
  443. }
  444. render() {
  445. const {
  446. radius,
  447. pointerRadius,
  448. pointerStrokeWidth,
  449. pointerFillColor,
  450. pointerStrokeColor,
  451. fillColor,
  452. trackStrokeWidth,
  453. trackStrokeColor,
  454. trackStrokeLinecap,
  455. strokeColor,
  456. strokeWidth,
  457. strokeLinecap,
  458. rotate,
  459. cut,
  460. trackTransition,
  461. transition,
  462. progress
  463. } = this.props
  464. const d = 2 * radius
  465. const width = d + this.getExtendedWidth()
  466. const circumference = 2 * Math.PI * radius
  467. const strokeLength = (circumference / 360) * (360 - cut)
  468. return (
  469. <figure
  470. className={`graph-widget`}
  471. style={{width: `${width || 250}px`}}
  472. data-value={progress}
  473. id={this.props.guageId}
  474. >
  475. <svg width={width} height={width}
  476. viewBox={`0 0 ${width} ${width}`}
  477. style={{ transform: `rotate(${rotate}deg)` }}
  478. >
  479. {trackStrokeWidth > 0 && (
  480. <circle
  481. cx={width / 2}
  482. cy={width / 2}
  483. r={radius}
  484. fill="none"
  485. stroke={trackStrokeColor}
  486. strokeWidth={trackStrokeWidth}
  487. strokeDasharray={this.getTrackStrokeDashArray(
  488. strokeLength,
  489. circumference
  490. )}
  491. strokeLinecap={trackStrokeLinecap}
  492. style={{ transition: trackTransition }}
  493. />
  494. )}
  495. {strokeWidth > 0 && (
  496. <circle
  497. cx={width / 2}
  498. cy={width / 2}
  499. r={radius}
  500. fill={fillColor}
  501. stroke={strokeColor}
  502. strokeWidth={strokeWidth}
  503. strokeDasharray={this.getStrokeDashArray(
  504. strokeLength,
  505. circumference
  506. )}
  507. strokeDashoffset={this.getStrokeDashoffset(
  508. strokeLength
  509. )}
  510. strokeLinecap={strokeLinecap}
  511. style={{ transition }}
  512. />
  513. )}
  514. {pointerRadius > 0 && (
  515. <circle
  516. cx={d}
  517. cy="50%"
  518. r={pointerRadius}
  519. fill={pointerFillColor}
  520. stroke={pointerStrokeColor}
  521. strokeWidth={pointerStrokeWidth}
  522. style={{
  523. transformOrigin: '50% 50%',
  524. transform: `rotate(${this.getPointerAngle()}deg) translate(${this.getExtendedWidth() /
  525. 2}px)`,
  526. transition
  527. }}
  528. />
  529. )}
  530. </svg>
  531. <figcaption className={`widget-value`}>
  532. {progress}%
  533. </figcaption>
  534. </figure>
  535. )
  536. }
  537. }
  538. ReactCustomizableProgressbar.defaultProps = {
  539. radius: 100,
  540. progress: 0,
  541. steps: 100,
  542. cut: 0,
  543. rotate: -90,
  544. strokeWidth: 20,
  545. strokeColor: 'indianred',
  546. fillColor: 'none',
  547. strokeLinecap: 'round',
  548. transition: '.3s ease',
  549. pointerRadius: 0,
  550. pointerStrokeWidth: 20,
  551. pointerStrokeColor: 'indianred',
  552. pointerFillColor: 'white',
  553. trackStrokeColor: '#e6e6e6',
  554. trackStrokeWidth: 20,
  555. trackStrokeLinecap: 'round',
  556. trackTransition: '.3s ease',
  557. counterClockwise: false,
  558. inverse: false,
  559. initialAnimation: false,
  560. initialAnimationDelay: 0
  561. };
  562. function MemoryUsagePanel(props) {
  563. return (
  564. <div className="widget-panel">
  565. <h3 className="widget-header">memory usage</h3>
  566. <div className="widget-value widget-info">
  567. <p><b>total memory:</b> {props.total}</p>
  568. <p><b>used memory:</b> {props.used}</p>
  569. <p><b>free memory:</b> {props.free}</p>
  570. { props.preload && <p><b>preload memory:</b> {props.preload}</p> }
  571. <p><b>wasted memory:</b> {props.wasted} ({props.wastedPercent}%)</p>
  572. { props.jitBuffer && <p><b>jit buffer:</b> {props.jitBuffer}</p> }
  573. { props.jitBufferFree && <p><b>jit buffer free:</b> {props.jitBufferFree} ({100 - props.jitBufferFreePercentage}%)</p> }
  574. </div>
  575. </div>
  576. );
  577. }
  578. function StatisticsPanel(props) {
  579. return (
  580. <div className="widget-panel">
  581. <h3 className="widget-header">opcache statistics</h3>
  582. <div className="widget-value widget-info">
  583. <p><b>number of cached files:</b> {props.num_cached_scripts}</p>
  584. <p><b>number of hits:</b> {props.hits}</p>
  585. <p><b>number of misses:</b> {props.misses}</p>
  586. <p><b>blacklist misses:</b> {props.blacklist_miss}</p>
  587. <p><b>number of cached keys:</b> {props.num_cached_keys}</p>
  588. <p><b>max cached keys:</b> {props.max_cached_keys}</p>
  589. </div>
  590. </div>
  591. );
  592. }
  593. function InternedStringsPanel(props) {
  594. return (
  595. <div className="widget-panel">
  596. <h3 className="widget-header">interned strings usage</h3>
  597. <div className="widget-value widget-info">
  598. <p><b>buffer size:</b> {props.buffer_size}</p>
  599. <p><b>used memory:</b> {props.strings_used_memory}</p>
  600. <p><b>free memory:</b> {props.strings_free_memory}</p>
  601. <p><b>number of strings:</b> {props.number_of_strings}</p>
  602. </div>
  603. </div>
  604. );
  605. }
  606. class CachedFiles extends React.Component {
  607. constructor(props) {
  608. super(props);
  609. this.doPagination = (typeof props.perPageLimit === "number"
  610. && props.perPageLimit > 0
  611. );
  612. this.state = {
  613. currentPage: 1,
  614. searchTerm: props.searchTerm,
  615. refreshPagination: 0,
  616. sortBy: `last_used_timestamp`,
  617. sortDir: `desc`
  618. }
  619. }
  620. setSearchTerm = debounce(searchTerm => {
  621. this.setState({
  622. searchTerm,
  623. refreshPagination: !(this.state.refreshPagination)
  624. });
  625. }, this.props.debounceRate);
  626. onPageChanged = currentPage => {
  627. this.setState({ currentPage });
  628. }
  629. handleInvalidate = e => {
  630. e.preventDefault();
  631. if (this.props.realtime) {
  632. axios.get(window.location.pathname, {params: { invalidate_searched: this.state.searchTerm }})
  633. .then((response) => {
  634. console.log('success: ' , response.data);
  635. });
  636. } else {
  637. window.location.href = e.currentTarget.href;
  638. }
  639. }
  640. changeSort = e => {
  641. this.setState({ [e.target.name]: e.target.value });
  642. }
  643. compareValues = (key, order = 'asc') => {
  644. return function innerSort(a, b) {
  645. if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) {
  646. return 0;
  647. }
  648. const varA = (typeof a[key] === 'string') ? a[key].toUpperCase() : a[key];
  649. const varB = (typeof b[key] === 'string') ? b[key].toUpperCase() : b[key];
  650. let comparison = 0;
  651. if (varA > varB) {
  652. comparison = 1;
  653. } else if (varA < varB) {
  654. comparison = -1;
  655. }
  656. return (
  657. (order === 'desc') ? (comparison * -1) : comparison
  658. );
  659. };
  660. }
  661. render() {
  662. if (!this.props.allow.fileList) {
  663. return null;
  664. }
  665. if (this.props.allFiles.length === 0) {
  666. return <p>No files have been cached or you have <i>opcache.file_cache_only</i> turned on</p>;
  667. }
  668. const { searchTerm, currentPage } = this.state;
  669. const offset = (currentPage - 1) * this.props.perPageLimit;
  670. const filesInSearch = (searchTerm
  671. ? this.props.allFiles.filter(file => {
  672. return !(file.full_path.indexOf(searchTerm) === -1);
  673. })
  674. : this.props.allFiles
  675. );
  676. filesInSearch.sort(this.compareValues(this.state.sortBy, this.state.sortDir));
  677. const filesInPage = (this.doPagination
  678. ? filesInSearch.slice(offset, offset + this.props.perPageLimit)
  679. : filesInSearch
  680. );
  681. const allFilesTotal = this.props.allFiles.length;
  682. const showingTotal = filesInSearch.length;
  683. return (
  684. <div>
  685. <form action="#">
  686. <label htmlFor="frmFilter">Start typing to filter on script path</label><br/>
  687. <input type="text" name="filter" id="frmFilter" className="file-filter" onChange={e => {this.setSearchTerm(e.target.value)}} />
  688. </form>
  689. <h3>{allFilesTotal} files cached{showingTotal !== allFilesTotal && `, ${showingTotal} showing due to filter '${this.state.searchTerm}'`}</h3>
  690. { this.props.allow.invalidate && this.state.searchTerm && showingTotal !== allFilesTotal &&
  691. <p><a href={`?invalidate_searched=${encodeURIComponent(this.state.searchTerm)}`} onClick={this.handleInvalidate}>Invalidate all matching files</a></p>
  692. }
  693. <div className="paginate-filter">
  694. {this.doPagination && <Pagination
  695. totalRecords={filesInSearch.length}
  696. pageLimit={this.props.perPageLimit}
  697. pageNeighbours={2}
  698. onPageChanged={this.onPageChanged}
  699. refresh={this.state.refreshPagination}
  700. />}
  701. <nav className="filter" aria-label="Sort order">
  702. <select name="sortBy" onChange={this.changeSort} value={this.state.sortBy}>
  703. <option value="last_used_timestamp">Last used</option>
  704. <option value="full_path">Path</option>
  705. <option value="hits">Number of hits</option>
  706. <option value="memory_consumption">Memory consumption</option>
  707. </select>
  708. <select name="sortDir" onChange={this.changeSort} value={this.state.sortDir}>
  709. <option value="desc">Descending</option>
  710. <option value="asc">Ascending</option>
  711. </select>
  712. </nav>
  713. </div>
  714. <table className="tables cached-list-table">
  715. <thead>
  716. <tr>
  717. <th>Script</th>
  718. </tr>
  719. </thead>
  720. <tbody>
  721. {filesInPage.map((file, index) => {
  722. return <CachedFile
  723. key={file.full_path}
  724. canInvalidate={this.props.allow.invalidate}
  725. realtime={this.props.realtime}
  726. {...file}
  727. />
  728. })}
  729. </tbody>
  730. </table>
  731. </div>
  732. );
  733. }
  734. }
  735. class CachedFile extends React.Component {
  736. handleInvalidate = e => {
  737. e.preventDefault();
  738. if (this.props.realtime) {
  739. axios.get(window.location.pathname, {params: { invalidate: e.currentTarget.getAttribute('data-file') }})
  740. .then((response) => {
  741. console.log('success: ' , response.data);
  742. });
  743. } else {
  744. window.location.href = e.currentTarget.href;
  745. }
  746. }
  747. render() {
  748. return (
  749. <tr data-path={this.props.full_path.toLowerCase()}>
  750. <td>
  751. <span className="file-pathname">{this.props.full_path}</span>
  752. <span className="file-metainfo">
  753. <b>hits: </b><span>{this.props.readable.hits}, </span>
  754. <b>memory: </b><span>{this.props.readable.memory_consumption}, </span>
  755. <b>last used: </b><span>{this.props.last_used}</span>
  756. </span>
  757. { !this.props.timestamp && <span className="invalid file-metainfo"> - has been invalidated</span> }
  758. { this.props.canInvalidate && <span>,&nbsp;<a className="file-metainfo"
  759. href={'?invalidate=' + this.props.full_path} data-file={this.props.full_path}
  760. onClick={this.handleInvalidate}>force file invalidation</a></span> }
  761. </td>
  762. </tr>
  763. );
  764. }
  765. }
  766. class IgnoredFiles extends React.Component {
  767. constructor(props) {
  768. super(props);
  769. this.doPagination = (typeof props.perPageLimit === "number"
  770. && props.perPageLimit > 0
  771. );
  772. this.state = {
  773. currentPage: 1,
  774. refreshPagination: 0
  775. }
  776. }
  777. onPageChanged = currentPage => {
  778. this.setState({ currentPage });
  779. }
  780. render() {
  781. if (!this.props.allow.fileList) {
  782. return null;
  783. }
  784. if (this.props.allFiles.length === 0) {
  785. return <p>No files have been ignored via <i>opcache.blacklist_filename</i></p>;
  786. }
  787. const { currentPage } = this.state;
  788. const offset = (currentPage - 1) * this.props.perPageLimit;
  789. const filesInPage = (this.doPagination
  790. ? this.props.allFiles.slice(offset, offset + this.props.perPageLimit)
  791. : this.props.allFiles
  792. );
  793. const allFilesTotal = this.props.allFiles.length;
  794. return (
  795. <div>
  796. <h3>{allFilesTotal} ignore file locations</h3>
  797. {this.doPagination && <Pagination
  798. totalRecords={allFilesTotal}
  799. pageLimit={this.props.perPageLimit}
  800. pageNeighbours={2}
  801. onPageChanged={this.onPageChanged}
  802. refresh={this.state.refreshPagination}
  803. />}
  804. <table className="tables ignored-list-table">
  805. <thead><tr><th>Path</th></tr></thead>
  806. <tbody>
  807. {filesInPage.map((file, index) => {
  808. return <tr key={file}><td>{file}</td></tr>
  809. })}
  810. </tbody>
  811. </table>
  812. </div>
  813. );
  814. }
  815. }
  816. class PreloadedFiles extends React.Component {
  817. constructor(props) {
  818. super(props);
  819. this.doPagination = (typeof props.perPageLimit === "number"
  820. && props.perPageLimit > 0
  821. );
  822. this.state = {
  823. currentPage: 1,
  824. refreshPagination: 0
  825. }
  826. }
  827. onPageChanged = currentPage => {
  828. this.setState({ currentPage });
  829. }
  830. render() {
  831. if (!this.props.allow.fileList) {
  832. return null;
  833. }
  834. if (this.props.allFiles.length === 0) {
  835. return <p>No files have been preloaded <i>opcache.preload</i></p>;
  836. }
  837. const { currentPage } = this.state;
  838. const offset = (currentPage - 1) * this.props.perPageLimit;
  839. const filesInPage = (this.doPagination
  840. ? this.props.allFiles.slice(offset, offset + this.props.perPageLimit)
  841. : this.props.allFiles
  842. );
  843. const allFilesTotal = this.props.allFiles.length;
  844. return (
  845. <div>
  846. <h3>{allFilesTotal} preloaded files</h3>
  847. {this.doPagination && <Pagination
  848. totalRecords={allFilesTotal}
  849. pageLimit={this.props.perPageLimit}
  850. pageNeighbours={2}
  851. onPageChanged={this.onPageChanged}
  852. refresh={this.state.refreshPagination}
  853. />}
  854. <table className="tables preload-list-table">
  855. <thead><tr><th>Path</th></tr></thead>
  856. <tbody>
  857. {filesInPage.map((file, index) => {
  858. return <tr key={file}><td>{file}</td></tr>
  859. })}
  860. </tbody>
  861. </table>
  862. </div>
  863. );
  864. }
  865. }
  866. class Pagination extends React.Component {
  867. constructor(props) {
  868. super(props);
  869. this.state = { currentPage: 1 };
  870. this.pageNeighbours =
  871. typeof props.pageNeighbours === "number"
  872. ? Math.max(0, Math.min(props.pageNeighbours, 2))
  873. : 0;
  874. }
  875. componentDidMount() {
  876. this.gotoPage(1);
  877. }
  878. componentDidUpdate(props) {
  879. const { refresh } = this.props;
  880. if (props.refresh !== refresh) {
  881. this.gotoPage(1);
  882. }
  883. }
  884. gotoPage = page => {
  885. const { onPageChanged = f => f } = this.props;
  886. const currentPage = Math.max(0, Math.min(page, this.totalPages()));
  887. this.setState({ currentPage }, () => onPageChanged(currentPage));
  888. };
  889. totalPages = () => {
  890. return Math.ceil(this.props.totalRecords / this.props.pageLimit);
  891. }
  892. handleClick = (page, evt) => {
  893. evt.preventDefault();
  894. this.gotoPage(page);
  895. };
  896. handleJumpLeft = evt => {
  897. evt.preventDefault();
  898. this.gotoPage(this.state.currentPage - this.pageNeighbours * 2 - 1);
  899. };
  900. handleJumpRight = evt => {
  901. evt.preventDefault();
  902. this.gotoPage(this.state.currentPage + this.pageNeighbours * 2 + 1);
  903. };
  904. handleMoveLeft = evt => {
  905. evt.preventDefault();
  906. this.gotoPage(this.state.currentPage - 1);
  907. };
  908. handleMoveRight = evt => {
  909. evt.preventDefault();
  910. this.gotoPage(this.state.currentPage + 1);
  911. };
  912. range = (from, to, step = 1) => {
  913. let i = from;
  914. const range = [];
  915. while (i <= to) {
  916. range.push(i);
  917. i += step;
  918. }
  919. return range;
  920. }
  921. fetchPageNumbers = () => {
  922. const totalPages = this.totalPages();
  923. const pageNeighbours = this.pageNeighbours;
  924. const totalNumbers = this.pageNeighbours * 2 + 3;
  925. const totalBlocks = totalNumbers + 2;
  926. if (totalPages > totalBlocks) {
  927. let pages = [];
  928. const leftBound = this.state.currentPage - pageNeighbours;
  929. const rightBound = this.state.currentPage + pageNeighbours;
  930. const beforeLastPage = totalPages - 1;
  931. const startPage = leftBound > 2 ? leftBound : 2;
  932. const endPage = rightBound < beforeLastPage ? rightBound : beforeLastPage;
  933. pages = this.range(startPage, endPage);
  934. const pagesCount = pages.length;
  935. const singleSpillOffset = totalNumbers - pagesCount - 1;
  936. const leftSpill = startPage > 2;
  937. const rightSpill = endPage < beforeLastPage;
  938. const leftSpillPage = "LEFT";
  939. const rightSpillPage = "RIGHT";
  940. if (leftSpill && !rightSpill) {
  941. const extraPages = this.range(startPage - singleSpillOffset, startPage - 1);
  942. pages = [leftSpillPage, ...extraPages, ...pages];
  943. } else if (!leftSpill && rightSpill) {
  944. const extraPages = this.range(endPage + 1, endPage + singleSpillOffset);
  945. pages = [...pages, ...extraPages, rightSpillPage];
  946. } else if (leftSpill && rightSpill) {
  947. pages = [leftSpillPage, ...pages, rightSpillPage];
  948. }
  949. return [1, ...pages, totalPages];
  950. }
  951. return this.range(1, totalPages);
  952. };
  953. render() {
  954. if (!this.props.totalRecords || this.totalPages() === 1) {
  955. return null
  956. }
  957. const { currentPage } = this.state;
  958. const pages = this.fetchPageNumbers();
  959. return (
  960. <nav aria-label="File list pagination">
  961. <ul className="pagination">
  962. {pages.map((page, index) => {
  963. if (page === "LEFT") {
  964. return (
  965. <React.Fragment key={index}>
  966. <li className="page-item arrow">
  967. <a className="page-link" href="#" aria-label="Previous" onClick={this.handleJumpLeft}>
  968. <span aria-hidden="true">↞</span>
  969. <span className="sr-only">Jump back</span>
  970. </a>
  971. </li>
  972. <li className="page-item arrow">
  973. <a className="page-link" href="#" aria-label="Previous" onClick={this.handleMoveLeft}>
  974. <span aria-hidden="true">⇠</span>
  975. <span className="sr-only">Previous page</span>
  976. </a>
  977. </li>
  978. </React.Fragment>
  979. );
  980. }
  981. if (page === "RIGHT") {
  982. return (
  983. <React.Fragment key={index}>
  984. <li className="page-item arrow">
  985. <a className="page-link" href="#" aria-label="Next" onClick={this.handleMoveRight}>
  986. <span aria-hidden="true">⇢</span>
  987. <span className="sr-only">Next page</span>
  988. </a>
  989. </li>
  990. <li className="page-item arrow">
  991. <a className="page-link" href="#" aria-label="Next" onClick={this.handleJumpRight}>
  992. <span aria-hidden="true">↠</span>
  993. <span className="sr-only">Jump forward</span>
  994. </a>
  995. </li>
  996. </React.Fragment>
  997. );
  998. }
  999. return (
  1000. <li key={index} className="page-item">
  1001. <a className={`page-link${currentPage === page ? " active" : ""}`} href="#" onClick={e => this.handleClick(page, e)}>
  1002. {page}
  1003. </a>
  1004. </li>
  1005. );
  1006. })}
  1007. </ul>
  1008. </nav>
  1009. );
  1010. }
  1011. }
  1012. function Footer(props) {
  1013. return (
  1014. <footer className="main-footer">
  1015. <a className="github-link" href="https://github.com/amnuts/opcache-gui"
  1016. target="_blank"
  1017. title="opcache-gui (currently version {props.version}) on GitHub"
  1018. >https://github.com/amnuts/opcache-gui - version {props.version}</a>
  1019. </footer>
  1020. );
  1021. }
  1022. function debounce(func, wait, immediate) {
  1023. let timeout;
  1024. wait = wait || 250;
  1025. return function() {
  1026. let context = this, args = arguments;
  1027. let later = function() {
  1028. timeout = null;
  1029. if (!immediate) {
  1030. func.apply(context, args);
  1031. }
  1032. };
  1033. let callNow = immediate && !timeout;
  1034. clearTimeout(timeout);
  1035. timeout = setTimeout(later, wait);
  1036. if (callNow) {
  1037. func.apply(context, args);
  1038. }
  1039. };
  1040. }