interface.jsx 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193
  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. return (
  356. <tr key={directive.k}>
  357. <td title={props.txt('View {0} manual entry', directive.k)}><a href={'https://php.net/manual/en/opcache.configuration.php#ini.'
  358. + (directive.k).replace(/_/g,'-')} target="_blank">{dShow}</a></td>
  359. <td>{vShow}</td>
  360. </tr>
  361. );
  362. });
  363. return (
  364. <table className="tables directives-table">
  365. <thead><tr><th colSpan="2">{props.txt('Directives')}</th></tr></thead>
  366. <tbody>{directiveNodes}</tbody>
  367. </table>
  368. );
  369. }
  370. function Functions(props) {
  371. return (
  372. <div id="functions">
  373. <table className="tables">
  374. <thead><tr><th>{props.txt('Available functions')}</th></tr></thead>
  375. <tbody>
  376. {props.functions.map(f =>
  377. <tr key={f}><td><a href={"https://php.net/"+f} title={props.txt('View manual page')} target="_blank">{f}</a></td></tr>
  378. )}
  379. </tbody>
  380. </table>
  381. </div>
  382. );
  383. }
  384. function UsageGraph(props) {
  385. const percentage = Math.round(((3.6 * props.value)/360)*100);
  386. return (props.charts
  387. ? <ReactCustomizableProgressbar
  388. progress={percentage}
  389. radius={100}
  390. strokeWidth={30}
  391. trackStrokeWidth={30}
  392. strokeColor={getComputedStyle(document.documentElement).getPropertyValue('--opcache-gui-graph-track-fill-color') || "#6CA6EF"}
  393. trackStrokeColor={getComputedStyle(document.documentElement).getPropertyValue('--opcache-gui-graph-track-background-color') || "#CCC"}
  394. gaugeId={props.gaugeId}
  395. />
  396. : <p className="widget-value"><span className="large">{percentage}</span><span>%</span></p>
  397. );
  398. }
  399. /**
  400. * This component is from <https://github.com/martyan/react-customizable-progressbar/>
  401. * MIT License (MIT), Copyright (c) 2019 Martin Juzl
  402. */
  403. class ReactCustomizableProgressbar extends React.Component {
  404. constructor(props) {
  405. super(props);
  406. this.state = {
  407. animationInited: false
  408. };
  409. }
  410. componentDidMount() {
  411. const { initialAnimation, initialAnimationDelay } = this.props
  412. if (initialAnimation)
  413. setTimeout(this.initAnimation, initialAnimationDelay)
  414. }
  415. initAnimation = () => {
  416. this.setState({ animationInited: true })
  417. }
  418. getProgress = () => {
  419. const { initialAnimation, progress } = this.props
  420. const { animationInited } = this.state
  421. return initialAnimation && !animationInited ? 0 : progress
  422. }
  423. getStrokeDashoffset = strokeLength => {
  424. const { counterClockwise, inverse, steps } = this.props
  425. const progress = this.getProgress()
  426. const progressLength = (strokeLength / steps) * (steps - progress)
  427. if (inverse) return counterClockwise ? 0 : progressLength - strokeLength
  428. return counterClockwise ? -1 * progressLength : progressLength
  429. }
  430. getStrokeDashArray = (strokeLength, circumference) => {
  431. const { counterClockwise, inverse, steps } = this.props
  432. const progress = this.getProgress()
  433. const progressLength = (strokeLength / steps) * (steps - progress)
  434. if (inverse) return `${progressLength}, ${circumference}`
  435. return counterClockwise
  436. ? `${strokeLength * (progress / 100)}, ${circumference}`
  437. : `${strokeLength}, ${circumference}`
  438. }
  439. getTrackStrokeDashArray = (strokeLength, circumference) => {
  440. const { initialAnimation } = this.props
  441. const { animationInited } = this.state
  442. if (initialAnimation && !animationInited) return `0, ${circumference}`
  443. return `${strokeLength}, ${circumference}`
  444. }
  445. getExtendedWidth = () => {
  446. const {
  447. strokeWidth,
  448. pointerRadius,
  449. pointerStrokeWidth,
  450. trackStrokeWidth
  451. } = this.props
  452. const pointerWidth = pointerRadius + pointerStrokeWidth
  453. if (pointerWidth > strokeWidth && pointerWidth > trackStrokeWidth) return pointerWidth * 2
  454. else if (strokeWidth > trackStrokeWidth) return strokeWidth * 2
  455. else return trackStrokeWidth * 2
  456. }
  457. getPointerAngle = () => {
  458. const { cut, counterClockwise, steps } = this.props
  459. const progress = this.getProgress()
  460. return counterClockwise
  461. ? ((360 - cut) / steps) * (steps - progress)
  462. : ((360 - cut) / steps) * progress
  463. }
  464. render() {
  465. const {
  466. radius,
  467. pointerRadius,
  468. pointerStrokeWidth,
  469. pointerFillColor,
  470. pointerStrokeColor,
  471. fillColor,
  472. trackStrokeWidth,
  473. trackStrokeColor,
  474. trackStrokeLinecap,
  475. strokeColor,
  476. strokeWidth,
  477. strokeLinecap,
  478. rotate,
  479. cut,
  480. trackTransition,
  481. transition,
  482. progress
  483. } = this.props
  484. const d = 2 * radius
  485. const width = d + this.getExtendedWidth()
  486. const circumference = 2 * Math.PI * radius
  487. const strokeLength = (circumference / 360) * (360 - cut)
  488. return (
  489. <figure
  490. className={`graph-widget`}
  491. style={{width: `${width || 250}px`}}
  492. data-value={progress}
  493. id={this.props.guageId}
  494. >
  495. <svg width={width} height={width}
  496. viewBox={`0 0 ${width} ${width}`}
  497. style={{ transform: `rotate(${rotate}deg)` }}
  498. >
  499. {trackStrokeWidth > 0 && (
  500. <circle
  501. cx={width / 2}
  502. cy={width / 2}
  503. r={radius}
  504. fill="none"
  505. stroke={trackStrokeColor}
  506. strokeWidth={trackStrokeWidth}
  507. strokeDasharray={this.getTrackStrokeDashArray(
  508. strokeLength,
  509. circumference
  510. )}
  511. strokeLinecap={trackStrokeLinecap}
  512. style={{ transition: trackTransition }}
  513. />
  514. )}
  515. {strokeWidth > 0 && (
  516. <circle
  517. cx={width / 2}
  518. cy={width / 2}
  519. r={radius}
  520. fill={fillColor}
  521. stroke={strokeColor}
  522. strokeWidth={strokeWidth}
  523. strokeDasharray={this.getStrokeDashArray(
  524. strokeLength,
  525. circumference
  526. )}
  527. strokeDashoffset={this.getStrokeDashoffset(
  528. strokeLength
  529. )}
  530. strokeLinecap={strokeLinecap}
  531. style={{ transition }}
  532. />
  533. )}
  534. {pointerRadius > 0 && (
  535. <circle
  536. cx={d}
  537. cy="50%"
  538. r={pointerRadius}
  539. fill={pointerFillColor}
  540. stroke={pointerStrokeColor}
  541. strokeWidth={pointerStrokeWidth}
  542. style={{
  543. transformOrigin: '50% 50%',
  544. transform: `rotate(${this.getPointerAngle()}deg) translate(${this.getExtendedWidth() /
  545. 2}px)`,
  546. transition
  547. }}
  548. />
  549. )}
  550. </svg>
  551. <figcaption className={`widget-value`}>
  552. {progress}%
  553. </figcaption>
  554. </figure>
  555. )
  556. }
  557. }
  558. ReactCustomizableProgressbar.defaultProps = {
  559. radius: 100,
  560. progress: 0,
  561. steps: 100,
  562. cut: 0,
  563. rotate: -90,
  564. strokeWidth: 20,
  565. strokeColor: 'indianred',
  566. fillColor: 'none',
  567. strokeLinecap: 'round',
  568. transition: '.3s ease',
  569. pointerRadius: 0,
  570. pointerStrokeWidth: 20,
  571. pointerStrokeColor: 'indianred',
  572. pointerFillColor: 'white',
  573. trackStrokeColor: '#e6e6e6',
  574. trackStrokeWidth: 20,
  575. trackStrokeLinecap: 'round',
  576. trackTransition: '.3s ease',
  577. counterClockwise: false,
  578. inverse: false,
  579. initialAnimation: false,
  580. initialAnimationDelay: 0
  581. };
  582. function MemoryUsagePanel(props) {
  583. return (
  584. <div className="widget-panel">
  585. <h3 className="widget-header">memory usage</h3>
  586. <div className="widget-value widget-info">
  587. <p><b>{props.txt('total memory')}:</b> {props.total}</p>
  588. <p><b>{props.txt('used memory')}:</b> {props.used}</p>
  589. <p><b>{props.txt('free memory')}:</b> {props.free}</p>
  590. { props.preload && <p><b>{props.txt('preload memory')}:</b> {props.preload}</p> }
  591. <p><b>{props.txt('wasted memory')}:</b> {props.wasted} ({props.wastedPercent}%)</p>
  592. { props.jitBuffer && <p><b>{props.txt('jit buffer')}:</b> {props.jitBuffer}</p> }
  593. { props.jitBufferFree && <p><b>{props.txt('jit buffer free')}:</b> {props.jitBufferFree} ({100 - props.jitBufferFreePercentage}%)</p> }
  594. </div>
  595. </div>
  596. );
  597. }
  598. function StatisticsPanel(props) {
  599. return (
  600. <div className="widget-panel">
  601. <h3 className="widget-header">{props.txt('opcache statistics')}</h3>
  602. <div className="widget-value widget-info">
  603. <p><b>{props.txt('number of cached')} files:</b> {props.num_cached_scripts}</p>
  604. <p><b>{props.txt('number of hits')}:</b> {props.hits}</p>
  605. <p><b>{props.txt('number of misses')}:</b> {props.misses}</p>
  606. <p><b>{props.txt('blacklist misses')}:</b> {props.blacklist_miss}</p>
  607. <p><b>{props.txt('number of cached keys')}:</b> {props.num_cached_keys}</p>
  608. <p><b>{props.txt('max cached keys')}:</b> {props.max_cached_keys}</p>
  609. </div>
  610. </div>
  611. );
  612. }
  613. function InternedStringsPanel(props) {
  614. return (
  615. <div className="widget-panel">
  616. <h3 className="widget-header">{props.txt('interned strings usage')}</h3>
  617. <div className="widget-value widget-info">
  618. <p><b>{props.txt('buffer size')}:</b> {props.buffer_size}</p>
  619. <p><b>{props.txt('used memory')}:</b> {props.strings_used_memory}</p>
  620. <p><b>{props.txt('free memory')}:</b> {props.strings_free_memory}</p>
  621. <p><b>{props.txt('number of strings')}:</b> {props.number_of_strings}</p>
  622. </div>
  623. </div>
  624. );
  625. }
  626. class CachedFiles extends React.Component {
  627. constructor(props) {
  628. super(props);
  629. this.doPagination = (typeof props.perPageLimit === "number"
  630. && props.perPageLimit > 0
  631. );
  632. this.state = {
  633. currentPage: 1,
  634. searchTerm: props.searchTerm,
  635. refreshPagination: 0,
  636. sortBy: `last_used_timestamp`,
  637. sortDir: `desc`
  638. }
  639. }
  640. setSearchTerm = debounce(searchTerm => {
  641. this.setState({
  642. searchTerm,
  643. refreshPagination: !(this.state.refreshPagination)
  644. });
  645. }, this.props.debounceRate);
  646. onPageChanged = currentPage => {
  647. this.setState({ currentPage });
  648. }
  649. handleInvalidate = e => {
  650. e.preventDefault();
  651. if (this.props.realtime) {
  652. axios.get(window.location.pathname, {params: { invalidate_searched: this.state.searchTerm }})
  653. .then((response) => {
  654. console.log('success: ' , response.data);
  655. });
  656. } else {
  657. window.location.href = e.currentTarget.href;
  658. }
  659. }
  660. changeSort = e => {
  661. this.setState({ [e.target.name]: e.target.value });
  662. }
  663. compareValues = (key, order = 'asc') => {
  664. return function innerSort(a, b) {
  665. if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) {
  666. return 0;
  667. }
  668. const varA = (typeof a[key] === 'string') ? a[key].toUpperCase() : a[key];
  669. const varB = (typeof b[key] === 'string') ? b[key].toUpperCase() : b[key];
  670. let comparison = 0;
  671. if (varA > varB) {
  672. comparison = 1;
  673. } else if (varA < varB) {
  674. comparison = -1;
  675. }
  676. return (
  677. (order === 'desc') ? (comparison * -1) : comparison
  678. );
  679. };
  680. }
  681. render() {
  682. if (!this.props.allow.fileList) {
  683. return null;
  684. }
  685. if (this.props.allFiles.length === 0) {
  686. return <p>{this.props.txt('No files have been cached or you have <i>opcache.file_cache_only</i> turned on')}</p>;
  687. }
  688. const { searchTerm, currentPage } = this.state;
  689. const offset = (currentPage - 1) * this.props.perPageLimit;
  690. const filesInSearch = (searchTerm
  691. ? this.props.allFiles.filter(file => {
  692. return !(file.full_path.indexOf(searchTerm) === -1);
  693. })
  694. : this.props.allFiles
  695. );
  696. filesInSearch.sort(this.compareValues(this.state.sortBy, this.state.sortDir));
  697. const filesInPage = (this.doPagination
  698. ? filesInSearch.slice(offset, offset + this.props.perPageLimit)
  699. : filesInSearch
  700. );
  701. const allFilesTotal = this.props.allFiles.length;
  702. const showingTotal = filesInSearch.length;
  703. const showing = showingTotal !== allFilesTotal ? ", {1} showing due to filter '{2}'" : "";
  704. return (
  705. <div>
  706. <form action="#">
  707. <label htmlFor="frmFilter">{this.props.txt('Start typing to filter on script path')}</label><br/>
  708. <input type="text" name="filter" id="frmFilter" className="file-filter" onChange={e => {this.setSearchTerm(e.target.value)}} />
  709. </form>
  710. <h3>{this.props.txt(`{0} files cached${showing}`, allFilesTotal, showingTotal, this.state.searchTerm)}</h3>
  711. { this.props.allow.invalidate && this.state.searchTerm && showingTotal !== allFilesTotal &&
  712. <p><a href={`?invalidate_searched=${encodeURIComponent(this.state.searchTerm)}`} onClick={this.handleInvalidate}>{this.props.txt('Invalidate all matching files')}</a></p>
  713. }
  714. <div className="paginate-filter">
  715. {this.doPagination && <Pagination
  716. totalRecords={filesInSearch.length}
  717. pageLimit={this.props.perPageLimit}
  718. pageNeighbours={2}
  719. onPageChanged={this.onPageChanged}
  720. refresh={this.state.refreshPagination}
  721. txt={this.props.txt}
  722. />}
  723. <nav className="filter" aria-label={this.props.txt('Sort order')}>
  724. <select name="sortBy" onChange={this.changeSort} value={this.state.sortBy}>
  725. <option value="last_used_timestamp">{this.props.txt('Last used')}</option>
  726. <option value="last_modified">{this.props.txt('Last modified')}</option>
  727. <option value="full_path">{this.props.txt('Path')}</option>
  728. <option value="hits">{this.props.txt('Number of hits')}</option>
  729. <option value="memory_consumption">{this.props.txt('Memory consumption')}</option>
  730. </select>
  731. <select name="sortDir" onChange={this.changeSort} value={this.state.sortDir}>
  732. <option value="desc">{this.props.txt('Descending')}</option>
  733. <option value="asc">{this.props.txt('Ascending')}</option>
  734. </select>
  735. </nav>
  736. </div>
  737. <table className="tables cached-list-table">
  738. <thead>
  739. <tr>
  740. <th>{this.props.txt('Script')}</th>
  741. </tr>
  742. </thead>
  743. <tbody>
  744. {filesInPage.map((file, index) => {
  745. return <CachedFile
  746. key={file.full_path}
  747. canInvalidate={this.props.allow.invalidate}
  748. realtime={this.props.realtime}
  749. txt={this.props.txt}
  750. {...file}
  751. />
  752. })}
  753. </tbody>
  754. </table>
  755. </div>
  756. );
  757. }
  758. }
  759. class CachedFile extends React.Component {
  760. handleInvalidate = e => {
  761. e.preventDefault();
  762. if (this.props.realtime) {
  763. axios.get(window.location.pathname, {params: { invalidate: e.currentTarget.getAttribute('data-file') }})
  764. .then((response) => {
  765. console.log('success: ' , response.data);
  766. });
  767. } else {
  768. window.location.href = e.currentTarget.href;
  769. }
  770. }
  771. render() {
  772. return (
  773. <tr data-path={this.props.full_path.toLowerCase()}>
  774. <td>
  775. <span className="file-pathname">{this.props.full_path}</span>
  776. <span className="file-metainfo">
  777. <b>{this.props.txt('hits')}: </b><span>{this.props.readable.hits}, </span>
  778. <b>{this.props.txt('memory')}: </b><span>{this.props.readable.memory_consumption}, </span>
  779. { this.props.last_modified && <><b>{this.props.txt('last modified')}: </b><span>{this.props.last_modified}, </span></> }
  780. <b>{this.props.txt('last used')}: </b><span>{this.props.last_used}</span>
  781. </span>
  782. { !this.props.timestamp && <span className="invalid file-metainfo"> - {this.props.txt('has been invalidated')}</span> }
  783. { this.props.canInvalidate && <span>,&nbsp;<a className="file-metainfo"
  784. href={'?invalidate=' + this.props.full_path} data-file={this.props.full_path}
  785. onClick={this.handleInvalidate}>{this.props.txt('force file invalidation')}</a></span> }
  786. </td>
  787. </tr>
  788. );
  789. }
  790. }
  791. class IgnoredFiles extends React.Component {
  792. constructor(props) {
  793. super(props);
  794. this.doPagination = (typeof props.perPageLimit === "number"
  795. && props.perPageLimit > 0
  796. );
  797. this.state = {
  798. currentPage: 1,
  799. refreshPagination: 0
  800. }
  801. }
  802. onPageChanged = currentPage => {
  803. this.setState({ currentPage });
  804. }
  805. render() {
  806. if (!this.props.allow.fileList) {
  807. return null;
  808. }
  809. if (this.props.allFiles.length === 0) {
  810. return <p>{this.props.txt('No files have been ignored via <i>opcache.blacklist_filename</i>')}</p>;
  811. }
  812. const { currentPage } = this.state;
  813. const offset = (currentPage - 1) * this.props.perPageLimit;
  814. const filesInPage = (this.doPagination
  815. ? this.props.allFiles.slice(offset, offset + this.props.perPageLimit)
  816. : this.props.allFiles
  817. );
  818. const allFilesTotal = this.props.allFiles.length;
  819. return (
  820. <div>
  821. <h3>{this.props.txt('{0} ignore file locations', allFilesTotal)}</h3>
  822. {this.doPagination && <Pagination
  823. totalRecords={allFilesTotal}
  824. pageLimit={this.props.perPageLimit}
  825. pageNeighbours={2}
  826. onPageChanged={this.onPageChanged}
  827. refresh={this.state.refreshPagination}
  828. txt={this.props.txt}
  829. />}
  830. <table className="tables ignored-list-table">
  831. <thead><tr><th>{this.props.txt('Path')}</th></tr></thead>
  832. <tbody>
  833. {filesInPage.map((file, index) => {
  834. return <tr key={file}><td>{file}</td></tr>
  835. })}
  836. </tbody>
  837. </table>
  838. </div>
  839. );
  840. }
  841. }
  842. class PreloadedFiles extends React.Component {
  843. constructor(props) {
  844. super(props);
  845. this.doPagination = (typeof props.perPageLimit === "number"
  846. && props.perPageLimit > 0
  847. );
  848. this.state = {
  849. currentPage: 1,
  850. refreshPagination: 0
  851. }
  852. }
  853. onPageChanged = currentPage => {
  854. this.setState({ currentPage });
  855. }
  856. render() {
  857. if (!this.props.allow.fileList) {
  858. return null;
  859. }
  860. if (this.props.allFiles.length === 0) {
  861. return <p>{this.props.txt('No files have been preloaded <i>opcache.preload</i>')}</p>;
  862. }
  863. const { currentPage } = this.state;
  864. const offset = (currentPage - 1) * this.props.perPageLimit;
  865. const filesInPage = (this.doPagination
  866. ? this.props.allFiles.slice(offset, offset + this.props.perPageLimit)
  867. : this.props.allFiles
  868. );
  869. const allFilesTotal = this.props.allFiles.length;
  870. return (
  871. <div>
  872. <h3>{this.props.txt('{0} preloaded files', allFilesTotal)}</h3>
  873. {this.doPagination && <Pagination
  874. totalRecords={allFilesTotal}
  875. pageLimit={this.props.perPageLimit}
  876. pageNeighbours={2}
  877. onPageChanged={this.onPageChanged}
  878. refresh={this.state.refreshPagination}
  879. txt={this.props.txt}
  880. />}
  881. <table className="tables preload-list-table">
  882. <thead><tr><th>{this.props.txt('Path')}</th></tr></thead>
  883. <tbody>
  884. {filesInPage.map((file, index) => {
  885. return <tr key={file}><td>{file}</td></tr>
  886. })}
  887. </tbody>
  888. </table>
  889. </div>
  890. );
  891. }
  892. }
  893. class Pagination extends React.Component {
  894. constructor(props) {
  895. super(props);
  896. this.state = { currentPage: 1 };
  897. this.pageNeighbours =
  898. typeof props.pageNeighbours === "number"
  899. ? Math.max(0, Math.min(props.pageNeighbours, 2))
  900. : 0;
  901. }
  902. componentDidMount() {
  903. this.gotoPage(1);
  904. }
  905. componentDidUpdate(props) {
  906. const { refresh } = this.props;
  907. if (props.refresh !== refresh) {
  908. this.gotoPage(1);
  909. }
  910. }
  911. gotoPage = page => {
  912. const { onPageChanged = f => f } = this.props;
  913. const currentPage = Math.max(0, Math.min(page, this.totalPages()));
  914. this.setState({ currentPage }, () => onPageChanged(currentPage));
  915. };
  916. totalPages = () => {
  917. return Math.ceil(this.props.totalRecords / this.props.pageLimit);
  918. }
  919. handleClick = (page, evt) => {
  920. evt.preventDefault();
  921. this.gotoPage(page);
  922. };
  923. handleJumpLeft = evt => {
  924. evt.preventDefault();
  925. this.gotoPage(this.state.currentPage - this.pageNeighbours * 2 - 1);
  926. };
  927. handleJumpRight = evt => {
  928. evt.preventDefault();
  929. this.gotoPage(this.state.currentPage + this.pageNeighbours * 2 + 1);
  930. };
  931. handleMoveLeft = evt => {
  932. evt.preventDefault();
  933. this.gotoPage(this.state.currentPage - 1);
  934. };
  935. handleMoveRight = evt => {
  936. evt.preventDefault();
  937. this.gotoPage(this.state.currentPage + 1);
  938. };
  939. range = (from, to, step = 1) => {
  940. let i = from;
  941. const range = [];
  942. while (i <= to) {
  943. range.push(i);
  944. i += step;
  945. }
  946. return range;
  947. }
  948. fetchPageNumbers = () => {
  949. const totalPages = this.totalPages();
  950. const pageNeighbours = this.pageNeighbours;
  951. const totalNumbers = this.pageNeighbours * 2 + 3;
  952. const totalBlocks = totalNumbers + 2;
  953. if (totalPages > totalBlocks) {
  954. let pages = [];
  955. const leftBound = this.state.currentPage - pageNeighbours;
  956. const rightBound = this.state.currentPage + pageNeighbours;
  957. const beforeLastPage = totalPages - 1;
  958. const startPage = leftBound > 2 ? leftBound : 2;
  959. const endPage = rightBound < beforeLastPage ? rightBound : beforeLastPage;
  960. pages = this.range(startPage, endPage);
  961. const pagesCount = pages.length;
  962. const singleSpillOffset = totalNumbers - pagesCount - 1;
  963. const leftSpill = startPage > 2;
  964. const rightSpill = endPage < beforeLastPage;
  965. const leftSpillPage = "LEFT";
  966. const rightSpillPage = "RIGHT";
  967. if (leftSpill && !rightSpill) {
  968. const extraPages = this.range(startPage - singleSpillOffset, startPage - 1);
  969. pages = [leftSpillPage, ...extraPages, ...pages];
  970. } else if (!leftSpill && rightSpill) {
  971. const extraPages = this.range(endPage + 1, endPage + singleSpillOffset);
  972. pages = [...pages, ...extraPages, rightSpillPage];
  973. } else if (leftSpill && rightSpill) {
  974. pages = [leftSpillPage, ...pages, rightSpillPage];
  975. }
  976. return [1, ...pages, totalPages];
  977. }
  978. return this.range(1, totalPages);
  979. };
  980. render() {
  981. if (!this.props.totalRecords || this.totalPages() === 1) {
  982. return null
  983. }
  984. const { currentPage } = this.state;
  985. const pages = this.fetchPageNumbers();
  986. return (
  987. <nav aria-label="File list pagination">
  988. <ul className="pagination">
  989. {pages.map((page, index) => {
  990. if (page === "LEFT") {
  991. return (
  992. <React.Fragment key={index}>
  993. <li className="page-item arrow">
  994. <a className="page-link" href="#" aria-label={this.props.txt('Previous')} onClick={this.handleJumpLeft}>
  995. <span aria-hidden="true">↞</span>
  996. <span className="sr-only">{this.props.txt('Jump back')}</span>
  997. </a>
  998. </li>
  999. <li className="page-item arrow">
  1000. <a className="page-link" href="#" aria-label={this.props.txt('Previous')} onClick={this.handleMoveLeft}>
  1001. <span aria-hidden="true">⇠</span>
  1002. <span className="sr-only">{this.props.txt('Previous page')}</span>
  1003. </a>
  1004. </li>
  1005. </React.Fragment>
  1006. );
  1007. }
  1008. if (page === "RIGHT") {
  1009. return (
  1010. <React.Fragment key={index}>
  1011. <li className="page-item arrow">
  1012. <a className="page-link" href="#" aria-label={this.props.txt('Next')} onClick={this.handleMoveRight}>
  1013. <span aria-hidden="true">⇢</span>
  1014. <span className="sr-only">{this.props.txt('Next page')}</span>
  1015. </a>
  1016. </li>
  1017. <li className="page-item arrow">
  1018. <a className="page-link" href="#" aria-label={this.props.txt('Next')} onClick={this.handleJumpRight}>
  1019. <span aria-hidden="true">↠</span>
  1020. <span className="sr-only">{this.props.txt('Jump forward')}</span>
  1021. </a>
  1022. </li>
  1023. </React.Fragment>
  1024. );
  1025. }
  1026. return (
  1027. <li key={index} className="page-item">
  1028. <a className={`page-link${currentPage === page ? " active" : ""}`} href="#" onClick={e => this.handleClick(page, e)}>
  1029. {page}
  1030. </a>
  1031. </li>
  1032. );
  1033. })}
  1034. </ul>
  1035. </nav>
  1036. );
  1037. }
  1038. }
  1039. function Footer(props) {
  1040. return (
  1041. <footer className="main-footer">
  1042. <a className="github-link" href="https://github.com/amnuts/opcache-gui"
  1043. target="_blank"
  1044. title="opcache-gui (currently version {props.version}) on GitHub"
  1045. >https://github.com/amnuts/opcache-gui - version {props.version}</a>
  1046. </footer>
  1047. );
  1048. }
  1049. function debounce(func, wait, immediate) {
  1050. let timeout;
  1051. wait = wait || 250;
  1052. return function() {
  1053. let context = this, args = arguments;
  1054. let later = function() {
  1055. timeout = null;
  1056. if (!immediate) {
  1057. func.apply(context, args);
  1058. }
  1059. };
  1060. let callNow = immediate && !timeout;
  1061. clearTimeout(timeout);
  1062. timeout = setTimeout(later, wait);
  1063. if (callNow) {
  1064. func.apply(context, args);
  1065. }
  1066. };
  1067. }