interface.jsx 45 KB

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