interface.jsx 41 KB

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