interface.jsx 45 KB

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