interface.jsx 41 KB

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