123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224 |
- class Interface extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- realtime: this.getCookie(),
- resetting: false,
- opstate: props.opstate
- }
- this.polling = false;
- this.isSecure = (window.location.protocol === 'https:');
- if (this.getCookie()) {
- this.startTimer();
- }
- }
- startTimer = () => {
- this.setState({realtime: true})
- this.polling = setInterval(() => {
- this.setState({fetching: true, resetting: false});
- axios.get(window.location.pathname, {time: Date.now()})
- .then((response) => {
- this.setState({opstate: response.data});
- });
- }, this.props.realtimeRefresh * 1000);
- }
- stopTimer = () => {
- this.setState({realtime: false, resetting: false})
- clearInterval(this.polling)
- }
- realtimeHandler = () => {
- const realtime = !this.state.realtime;
- if (!realtime) {
- this.stopTimer();
- this.removeCookie();
- } else {
- this.startTimer();
- this.setCookie();
- }
- }
- resetHandler = () => {
- if (this.state.realtime) {
- this.setState({resetting: true});
- axios.get(window.location.pathname, {params: {reset: 1}})
- .then((response) => {
- console.log('success: ', response.data);
- });
- } else {
- window.location.href = '?reset=1';
- }
- }
- setCookie = () => {
- let d = new Date();
- d.setTime(d.getTime() + (this.props.cookie.ttl * 86400000));
- document.cookie = `${this.props.cookie.name}=true;expires=${d.toUTCString()};path=/${this.isSecure ? ';secure' : ''}`;
- }
- removeCookie = () => {
- document.cookie = `${this.props.cookie.name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/${this.isSecure ? ';secure' : ''}`;
- }
- getCookie = () => {
- const v = document.cookie.match(`(^|;) ?${this.props.cookie.name}=([^;]*)(;|$)`);
- return v ? !!v[2] : false;
- };
- txt = (text, ...args) => {
- if (this.props.language !== null && this.props.language.hasOwnProperty(text) && this.props.language[text]) {
- text = this.props.language[text];
- }
- args.forEach((arg, i) => {
- text = text.replaceAll(`{${i}}`, arg);
- });
- return text;
- };
- render() {
- const { opstate, realtimeRefresh, ...otherProps } = this.props;
- return (
- <>
- <header>
- <MainNavigation {...otherProps}
- opstate={this.state.opstate}
- realtime={this.state.realtime}
- resetting={this.state.resetting}
- realtimeHandler={this.realtimeHandler}
- resetHandler={this.resetHandler}
- txt={this.txt}
- />
- </header>
- <Footer
- version={this.props.opstate.version.gui}
- txt={this.txt}
- />
- </>
- );
- }
- }
- function MainNavigation(props) {
- return (
- <nav className="main-nav">
- <Tabs>
- <div label={props.txt("Overview")} tabId="overview" tabIndex={1}>
- <OverviewCounts
- overview={props.opstate.overview}
- highlight={props.highlight}
- useCharts={props.useCharts}
- txt={props.txt}
- />
- <div id="info" className="tab-content-overview-info">
- <GeneralInfo
- start={props.opstate.overview && props.opstate.overview.readable.start_time || null}
- reset={props.opstate.overview && props.opstate.overview.readable.last_restart_time || null}
- version={props.opstate.version}
- jit={props.opstate.jitState}
- txt={props.txt}
- />
- <Directives
- directives={props.opstate.directives}
- txt={props.txt}
- />
- <Functions
- functions={props.opstate.functions}
- txt={props.txt}
- />
- </div>
- </div>
- {
- props.allow.filelist &&
- <div label={props.txt("Cached")} tabId="cached" tabIndex={2}>
- <CachedFiles
- perPageLimit={props.perPageLimit}
- allFiles={props.opstate.files}
- searchTerm={props.searchTerm}
- debounceRate={props.debounceRate}
- allow={{fileList: props.allow.filelist, invalidate: props.allow.invalidate}}
- realtime={props.realtime}
- txt={props.txt}
- />
- </div>
- }
- {
- (props.allow.filelist && props.opstate.blacklist.length &&
- <div label={props.txt("Ignored")} tabId="ignored" tabIndex={3}>
- <IgnoredFiles
- perPageLimit={props.perPageLimit}
- allFiles={props.opstate.blacklist}
- allow={{fileList: props.allow.filelist }}
- txt={props.txt}
- />
- </div>)
- }
- {
- (props.allow.filelist && props.opstate.preload.length &&
- <div label={props.txt("Preloaded")} tabId="preloaded" tabIndex={4}>
- <PreloadedFiles
- perPageLimit={props.perPageLimit}
- allFiles={props.opstate.preload}
- allow={{fileList: props.allow.filelist }}
- txt={props.txt}
- />
- </div>)
- }
- {
- props.allow.reset &&
- <div label={props.txt("Reset cache")} tabId="resetCache"
- className={`nav-tab-link-reset${props.resetting ? ' is-resetting pulse' : ''}`}
- handler={props.resetHandler}
- tabIndex={5}
- ></div>
- }
- {
- props.allow.realtime &&
- <div label={props.txt(`${props.realtime ? 'Disable' : 'Enable'} real-time update`)} tabId="toggleRealtime"
- className={`nav-tab-link-realtime${props.realtime ? ' live-update pulse' : ''}`}
- handler={props.realtimeHandler}
- tabIndex={6}
- ></div>
- }
- </Tabs>
- </nav>
- );
- }
- class Tabs extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- activeTab: this.props.children[0].props.label,
- };
- }
- onClickTabItem = (tab) => {
- this.setState({ activeTab: tab });
- }
- render() {
- const {
- onClickTabItem,
- state: { activeTab }
- } = this;
- const children = this.props.children.filter(Boolean);
- return (
- <>
- <ul className="nav-tab-list">
- {children.map((child) => {
- const { tabId, label, className, handler, tabIndex } = child.props;
- return (
- <Tab
- activeTab={activeTab}
- key={tabId}
- label={label}
- onClick={handler || onClickTabItem}
- className={className}
- tabIndex={tabIndex}
- tabId={tabId}
- />
- );
- })}
- </ul>
- <div className="tab-content">
- {children.map((child) => (
- <div key={child.props.label}
- style={{ display: child.props.label === activeTab ? 'block' : 'none' }}
- id={`${child.props.tabId}-content`}
- >
- {child.props.children}
- </div>
- ))}
- </div>
- </>
- );
- }
- }
- class Tab extends React.Component {
- onClick = () => {
- const { label, onClick } = this.props;
- onClick(label);
- }
- render() {
- const {
- onClick,
- props: { activeTab, label, tabIndex, tabId },
- } = this;
- let className = 'nav-tab';
- if (this.props.className) {
- className += ` ${this.props.className}`;
- }
- if (activeTab === label) {
- className += ' active';
- }
- return (
- <li className={className}
- onClick={onClick}
- tabIndex={tabIndex}
- role="tab"
- aria-controls={`${tabId}-content`}
- >{label}</li>
- );
- }
- }
- function OverviewCounts(props) {
- if (props.overview === false) {
- return (
- <p class="file-cache-only">
- {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>.`)}
- </p>
- );
- }
- const graphList = [
- {id: 'memoryUsageCanvas', title: props.txt('memory'), show: props.highlight.memory, value: props.overview.used_memory_percentage},
- {id: 'hitRateCanvas', title: props.txt('hit rate'), show: props.highlight.hits, value: props.overview.hit_rate_percentage},
- {id: 'keyUsageCanvas', title: props.txt('keys'), show: props.highlight.keys, value: props.overview.used_key_percentage},
- {id: 'jitUsageCanvas', title: props.txt('jit buffer'), show: props.highlight.jit, value: props.overview.jit_buffer_used_percentage}
- ];
- return (
- <div id="counts" className="tab-content-overview-counts">
- {graphList.map((graph) => {
- if (!graph.show) {
- return null;
- }
- return (
- <div className="widget-panel" key={graph.id}>
- <h3 className="widget-header">{graph.title}</h3>
- <UsageGraph charts={props.useCharts} value={graph.value} gaugeId={graph.id} />
- </div>
- );
- })}
- <MemoryUsagePanel
- total={props.overview.readable.total_memory}
- used={props.overview.readable.used_memory}
- free={props.overview.readable.free_memory}
- wasted={props.overview.readable.wasted_memory}
- preload={props.overview.readable.preload_memory || null}
- wastedPercent={props.overview.wasted_percentage}
- jitBuffer={props.overview.readable.jit_buffer_size || null}
- jitBufferFree={props.overview.readable.jit_buffer_free || null}
- jitBufferFreePercentage={props.overview.jit_buffer_used_percentage || null}
- txt={props.txt}
- />
- <StatisticsPanel
- num_cached_scripts={props.overview.readable.num_cached_scripts}
- hits={props.overview.readable.hits}
- misses={props.overview.readable.misses}
- blacklist_miss={props.overview.readable.blacklist_miss}
- num_cached_keys={props.overview.readable.num_cached_keys}
- max_cached_keys={props.overview.readable.max_cached_keys}
- txt={props.txt}
- />
- {props.overview.readable.interned &&
- <InternedStringsPanel
- buffer_size={props.overview.readable.interned.buffer_size}
- strings_used_memory={props.overview.readable.interned.strings_used_memory}
- strings_free_memory={props.overview.readable.interned.strings_free_memory}
- number_of_strings={props.overview.readable.interned.number_of_strings}
- txt={props.txt}
- />
- }
- </div>
- );
- }
- function GeneralInfo(props) {
- return (
- <table className="tables general-info-table">
- <thead>
- <tr><th colSpan="2">{props.txt('General info')}</th></tr>
- </thead>
- <tbody>
- <tr><td>Zend OPcache</td><td>{props.version.version}</td></tr>
- <tr><td>PHP</td><td>{props.version.php}</td></tr>
- <tr><td>{props.txt('Host')}</td><td>{props.version.host}</td></tr>
- <tr><td>{props.txt('Server Software')}</td><td>{props.version.server}</td></tr>
- { props.start ? <tr><td>{props.txt('Start time')}</td><td>{props.start}</td></tr> : null }
- { props.reset ? <tr><td>{props.txt('Last reset')}</td><td>{props.reset}</td></tr> : null }
- <tr>
- <td>{props.txt('JIT enabled')}</td>
- <td>
- {props.txt(props.jit.enabled ? "Yes" : "No")}
- {props.jit.reason && (<span dangerouslySetInnerHTML={{__html: ` (${props.jit.reason})` }} />)}
- </td>
- </tr>
- </tbody>
- </table>
- );
- }
- function Directives(props) {
- let directiveList = (directive) => {
- return (
- <ul className="directive-list">{
- directive.v.map((item, key) => {
- return Array.isArray(item)
- ? <li key={"sublist_" + key}>{directiveList({v:item})}</li>
- : <li key={key}>{item}</li>
- })
- }</ul>
- );
- };
- let directiveNodes = props.directives.map(function(directive) {
- let map = { 'opcache.':'', '_':' ' };
- let dShow = directive.k.replace(/opcache\.|_/gi, function(matched){
- return map[matched];
- });
- let vShow;
- if (directive.v === true || directive.v === false) {
- vShow = React.createElement('i', {}, props.txt(directive.v.toString()));
- } else if (directive.v === '') {
- vShow = React.createElement('i', {}, props.txt('no value'));
- } else {
- if (Array.isArray(directive.v)) {
- vShow = directiveList(directive);
- } else {
- vShow = directive.v;
- }
- }
- let directiveLink = (name) => {
- if (name === 'opcache.jit_max_recursive_returns') {
- return 'opcache.jit-max-recursive-return';
- }
- return (
- [
- 'opcache.file_update_protection',
- 'opcache.huge_code_pages',
- 'opcache.lockfile_path',
- 'opcache.opt_debug_level',
- ].includes(name)
- ? name
- : name.replace(/_/g,'-')
- );
- }
- return (
- <tr key={directive.k}>
- <td title={props.txt('View {0} manual entry', directive.k)}><a href={'https://php.net/manual/en/opcache.configuration.php#ini.'
- + directiveLink(directive.k)} target="_blank">{dShow}</a></td>
- <td>{vShow}</td>
- </tr>
- );
- });
- return (
- <table className="tables directives-table">
- <thead><tr><th colSpan="2">{props.txt('Directives')}</th></tr></thead>
- <tbody>{directiveNodes}</tbody>
- </table>
- );
- }
- function Functions(props) {
- return (
- <div id="functions">
- <table className="tables">
- <thead><tr><th>{props.txt('Available functions')}</th></tr></thead>
- <tbody>
- {props.functions.map(f =>
- <tr key={f}><td><a href={"https://php.net/"+f} title={props.txt('View manual page')} target="_blank">{f}</a></td></tr>
- )}
- </tbody>
- </table>
- </div>
- );
- }
- function UsageGraph(props) {
- const percentage = Math.round(((3.6 * props.value)/360)*100);
- return (props.charts
- ? <ReactCustomizableProgressbar
- progress={percentage}
- radius={100}
- strokeWidth={30}
- trackStrokeWidth={30}
- strokeColor={getComputedStyle(document.documentElement).getPropertyValue('--opcache-gui-graph-track-fill-color') || "#6CA6EF"}
- trackStrokeColor={getComputedStyle(document.documentElement).getPropertyValue('--opcache-gui-graph-track-background-color') || "#CCC"}
- gaugeId={props.gaugeId}
- />
- : <p className="widget-value"><span className="large">{percentage}</span><span>%</span></p>
- );
- }
- /**
- * This component is from <https://github.com/martyan/react-customizable-progressbar/>
- * MIT License (MIT), Copyright (c) 2019 Martin Juzl
- */
- class ReactCustomizableProgressbar extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- animationInited: false
- };
- }
- componentDidMount() {
- const { initialAnimation, initialAnimationDelay } = this.props
- if (initialAnimation)
- setTimeout(this.initAnimation, initialAnimationDelay)
- }
- initAnimation = () => {
- this.setState({ animationInited: true })
- }
- getProgress = () => {
- const { initialAnimation, progress } = this.props
- const { animationInited } = this.state
- return initialAnimation && !animationInited ? 0 : progress
- }
- getStrokeDashoffset = strokeLength => {
- const { counterClockwise, inverse, steps } = this.props
- const progress = this.getProgress()
- const progressLength = (strokeLength / steps) * (steps - progress)
- if (inverse) return counterClockwise ? 0 : progressLength - strokeLength
- return counterClockwise ? -1 * progressLength : progressLength
- }
- getStrokeDashArray = (strokeLength, circumference) => {
- const { counterClockwise, inverse, steps } = this.props
- const progress = this.getProgress()
- const progressLength = (strokeLength / steps) * (steps - progress)
- if (inverse) return `${progressLength}, ${circumference}`
- return counterClockwise
- ? `${strokeLength * (progress / 100)}, ${circumference}`
- : `${strokeLength}, ${circumference}`
- }
- getTrackStrokeDashArray = (strokeLength, circumference) => {
- const { initialAnimation } = this.props
- const { animationInited } = this.state
- if (initialAnimation && !animationInited) return `0, ${circumference}`
- return `${strokeLength}, ${circumference}`
- }
- getExtendedWidth = () => {
- const {
- strokeWidth,
- pointerRadius,
- pointerStrokeWidth,
- trackStrokeWidth
- } = this.props
- const pointerWidth = pointerRadius + pointerStrokeWidth
- if (pointerWidth > strokeWidth && pointerWidth > trackStrokeWidth) return pointerWidth * 2
- else if (strokeWidth > trackStrokeWidth) return strokeWidth * 2
- else return trackStrokeWidth * 2
- }
- getPointerAngle = () => {
- const { cut, counterClockwise, steps } = this.props
- const progress = this.getProgress()
- return counterClockwise
- ? ((360 - cut) / steps) * (steps - progress)
- : ((360 - cut) / steps) * progress
- }
- render() {
- const {
- radius,
- pointerRadius,
- pointerStrokeWidth,
- pointerFillColor,
- pointerStrokeColor,
- fillColor,
- trackStrokeWidth,
- trackStrokeColor,
- trackStrokeLinecap,
- strokeColor,
- strokeWidth,
- strokeLinecap,
- rotate,
- cut,
- trackTransition,
- transition,
- progress
- } = this.props
- const d = 2 * radius
- const width = d + this.getExtendedWidth()
- const circumference = 2 * Math.PI * radius
- const strokeLength = (circumference / 360) * (360 - cut)
- return (
- <figure
- className={`graph-widget`}
- style={{width: `${width || 250}px`}}
- data-value={progress}
- id={this.props.guageId}
- >
- <svg width={width} height={width}
- viewBox={`0 0 ${width} ${width}`}
- style={{ transform: `rotate(${rotate}deg)` }}
- >
- {trackStrokeWidth > 0 && (
- <circle
- cx={width / 2}
- cy={width / 2}
- r={radius}
- fill="none"
- stroke={trackStrokeColor}
- strokeWidth={trackStrokeWidth}
- strokeDasharray={this.getTrackStrokeDashArray(
- strokeLength,
- circumference
- )}
- strokeLinecap={trackStrokeLinecap}
- style={{ transition: trackTransition }}
- />
- )}
- {strokeWidth > 0 && (
- <circle
- cx={width / 2}
- cy={width / 2}
- r={radius}
- fill={fillColor}
- stroke={strokeColor}
- strokeWidth={strokeWidth}
- strokeDasharray={this.getStrokeDashArray(
- strokeLength,
- circumference
- )}
- strokeDashoffset={this.getStrokeDashoffset(
- strokeLength
- )}
- strokeLinecap={strokeLinecap}
- style={{ transition }}
- />
- )}
- {pointerRadius > 0 && (
- <circle
- cx={d}
- cy="50%"
- r={pointerRadius}
- fill={pointerFillColor}
- stroke={pointerStrokeColor}
- strokeWidth={pointerStrokeWidth}
- style={{
- transformOrigin: '50% 50%',
- transform: `rotate(${this.getPointerAngle()}deg) translate(${this.getExtendedWidth() /
- 2}px)`,
- transition
- }}
- />
- )}
- </svg>
- <figcaption className={`widget-value`}>
- {progress}%
- </figcaption>
- </figure>
- )
- }
- }
- ReactCustomizableProgressbar.defaultProps = {
- radius: 100,
- progress: 0,
- steps: 100,
- cut: 0,
- rotate: -90,
- strokeWidth: 20,
- strokeColor: 'indianred',
- fillColor: 'none',
- strokeLinecap: 'round',
- transition: '.3s ease',
- pointerRadius: 0,
- pointerStrokeWidth: 20,
- pointerStrokeColor: 'indianred',
- pointerFillColor: 'white',
- trackStrokeColor: '#e6e6e6',
- trackStrokeWidth: 20,
- trackStrokeLinecap: 'round',
- trackTransition: '.3s ease',
- counterClockwise: false,
- inverse: false,
- initialAnimation: false,
- initialAnimationDelay: 0
- };
- function MemoryUsagePanel(props) {
- return (
- <div className="widget-panel">
- <h3 className="widget-header">memory usage</h3>
- <div className="widget-value widget-info">
- <p><b>{props.txt('total memory')}:</b> {props.total}</p>
- <p><b>{props.txt('used memory')}:</b> {props.used}</p>
- <p><b>{props.txt('free memory')}:</b> {props.free}</p>
- { props.preload && <p><b>{props.txt('preload memory')}:</b> {props.preload}</p> }
- <p><b>{props.txt('wasted memory')}:</b> {props.wasted} ({props.wastedPercent}%)</p>
- { props.jitBuffer && <p><b>{props.txt('jit buffer')}:</b> {props.jitBuffer}</p> }
- { props.jitBufferFree && <p><b>{props.txt('jit buffer free')}:</b> {props.jitBufferFree} ({100 - props.jitBufferFreePercentage}%)</p> }
- </div>
- </div>
- );
- }
- function StatisticsPanel(props) {
- return (
- <div className="widget-panel">
- <h3 className="widget-header">{props.txt('opcache statistics')}</h3>
- <div className="widget-value widget-info">
- <p><b>{props.txt('number of cached')} files:</b> {props.num_cached_scripts}</p>
- <p><b>{props.txt('number of hits')}:</b> {props.hits}</p>
- <p><b>{props.txt('number of misses')}:</b> {props.misses}</p>
- <p><b>{props.txt('blacklist misses')}:</b> {props.blacklist_miss}</p>
- <p><b>{props.txt('number of cached keys')}:</b> {props.num_cached_keys}</p>
- <p><b>{props.txt('max cached keys')}:</b> {props.max_cached_keys}</p>
- </div>
- </div>
- );
- }
- function InternedStringsPanel(props) {
- return (
- <div className="widget-panel">
- <h3 className="widget-header">{props.txt('interned strings usage')}</h3>
- <div className="widget-value widget-info">
- <p><b>{props.txt('buffer size')}:</b> {props.buffer_size}</p>
- <p><b>{props.txt('used memory')}:</b> {props.strings_used_memory}</p>
- <p><b>{props.txt('free memory')}:</b> {props.strings_free_memory}</p>
- <p><b>{props.txt('number of strings')}:</b> {props.number_of_strings}</p>
- </div>
- </div>
- );
- }
- class CachedFiles extends React.Component {
- constructor(props) {
- super(props);
- this.doPagination = (typeof props.perPageLimit === "number"
- && props.perPageLimit > 0
- );
- this.state = {
- currentPage: 1,
- searchTerm: props.searchTerm,
- refreshPagination: 0,
- sortBy: `last_used_timestamp`,
- sortDir: `desc`
- }
- }
- setSearchTerm = debounce(searchTerm => {
- this.setState({
- searchTerm,
- refreshPagination: !(this.state.refreshPagination)
- });
- }, this.props.debounceRate);
- onPageChanged = currentPage => {
- this.setState({ currentPage });
- }
- handleInvalidate = e => {
- e.preventDefault();
- if (this.props.realtime) {
- axios.get(window.location.pathname, {params: { invalidate_searched: this.state.searchTerm }})
- .then((response) => {
- console.log('success: ' , response.data);
- });
- } else {
- window.location.href = e.currentTarget.href;
- }
- }
- changeSort = e => {
- this.setState({ [e.target.name]: e.target.value });
- }
- compareValues = (key, order = 'asc') => {
- return function innerSort(a, b) {
- if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) {
- return 0;
- }
- const varA = (typeof a[key] === 'string') ? a[key].toUpperCase() : a[key];
- const varB = (typeof b[key] === 'string') ? b[key].toUpperCase() : b[key];
- let comparison = 0;
- if (varA > varB) {
- comparison = 1;
- } else if (varA < varB) {
- comparison = -1;
- }
- return (
- (order === 'desc') ? (comparison * -1) : comparison
- );
- };
- }
- render() {
- if (!this.props.allow.fileList) {
- return null;
- }
- if (this.props.allFiles.length === 0) {
- return <p>{this.props.txt('No files have been cached or you have <i>opcache.file_cache_only</i> turned on')}</p>;
- }
- const { searchTerm, currentPage } = this.state;
- const offset = (currentPage - 1) * this.props.perPageLimit;
- const filesInSearch = (searchTerm
- ? this.props.allFiles.filter(file => {
- return !(file.full_path.indexOf(searchTerm) === -1);
- })
- : this.props.allFiles
- );
- filesInSearch.sort(this.compareValues(this.state.sortBy, this.state.sortDir));
- const filesInPage = (this.doPagination
- ? filesInSearch.slice(offset, offset + this.props.perPageLimit)
- : filesInSearch
- );
- const allFilesTotal = this.props.allFiles.length;
- const showingTotal = filesInSearch.length;
- const showing = showingTotal !== allFilesTotal ? ", {1} showing due to filter '{2}'" : "";
- return (
- <div>
- <form action="#">
- <label htmlFor="frmFilter">{this.props.txt('Start typing to filter on script path')}</label><br/>
- <input type="text" name="filter" id="frmFilter" className="file-filter" onChange={e => {this.setSearchTerm(e.target.value)}} />
- </form>
- <h3>{this.props.txt(`{0} files cached${showing}`, allFilesTotal, showingTotal, this.state.searchTerm)}</h3>
- { this.props.allow.invalidate && this.state.searchTerm && showingTotal !== allFilesTotal &&
- <p><a href={`?invalidate_searched=${encodeURIComponent(this.state.searchTerm)}`} onClick={this.handleInvalidate}>{this.props.txt('Invalidate all matching files')}</a></p>
- }
- <div className="paginate-filter">
- {this.doPagination && <Pagination
- totalRecords={filesInSearch.length}
- pageLimit={this.props.perPageLimit}
- pageNeighbours={2}
- onPageChanged={this.onPageChanged}
- refresh={this.state.refreshPagination}
- txt={this.props.txt}
- />}
- <nav className="filter" aria-label={this.props.txt('Sort order')}>
- <select name="sortBy" onChange={this.changeSort} value={this.state.sortBy}>
- <option value="last_used_timestamp">{this.props.txt('Last used')}</option>
- <option value="last_modified">{this.props.txt('Last modified')}</option>
- <option value="full_path">{this.props.txt('Path')}</option>
- <option value="hits">{this.props.txt('Number of hits')}</option>
- <option value="memory_consumption">{this.props.txt('Memory consumption')}</option>
- </select>
- <select name="sortDir" onChange={this.changeSort} value={this.state.sortDir}>
- <option value="desc">{this.props.txt('Descending')}</option>
- <option value="asc">{this.props.txt('Ascending')}</option>
- </select>
- </nav>
- </div>
- <table className="tables cached-list-table">
- <thead>
- <tr>
- <th>{this.props.txt('Script')}</th>
- </tr>
- </thead>
- <tbody>
- {filesInPage.map((file, index) => {
- return <CachedFile
- key={file.full_path}
- canInvalidate={this.props.allow.invalidate}
- realtime={this.props.realtime}
- txt={this.props.txt}
- {...file}
- />
- })}
- </tbody>
- </table>
- </div>
- );
- }
- }
- class CachedFile extends React.Component {
- handleInvalidate = e => {
- e.preventDefault();
- if (this.props.realtime) {
- axios.get(window.location.pathname, {params: { invalidate: e.currentTarget.getAttribute('data-file') }})
- .then((response) => {
- console.log('success: ' , response.data);
- });
- } else {
- window.location.href = e.currentTarget.href;
- }
- }
- render() {
- return (
- <tr data-path={this.props.full_path.toLowerCase()}>
- <td>
- <span className="file-pathname">{this.props.full_path}</span>
- <span className="file-metainfo">
- <b>{this.props.txt('hits')}: </b><span>{this.props.readable.hits}, </span>
- <b>{this.props.txt('memory')}: </b><span>{this.props.readable.memory_consumption}, </span>
- { this.props.last_modified && <><b>{this.props.txt('last modified')}: </b><span>{this.props.last_modified}, </span></> }
- <b>{this.props.txt('last used')}: </b><span>{this.props.last_used}</span>
- </span>
- { !this.props.timestamp && <span className="invalid file-metainfo"> - {this.props.txt('has been invalidated')}</span> }
- { this.props.canInvalidate && <span>, <a className="file-metainfo"
- href={'?invalidate=' + this.props.full_path} data-file={this.props.full_path}
- onClick={this.handleInvalidate}>{this.props.txt('force file invalidation')}</a></span> }
- </td>
- </tr>
- );
- }
- }
- class IgnoredFiles extends React.Component {
- constructor(props) {
- super(props);
- this.doPagination = (typeof props.perPageLimit === "number"
- && props.perPageLimit > 0
- );
- this.state = {
- currentPage: 1,
- refreshPagination: 0
- }
- }
- onPageChanged = currentPage => {
- this.setState({ currentPage });
- }
- render() {
- if (!this.props.allow.fileList) {
- return null;
- }
- if (this.props.allFiles.length === 0) {
- return <p>{this.props.txt('No files have been ignored via <i>opcache.blacklist_filename</i>')}</p>;
- }
- const { currentPage } = this.state;
- const offset = (currentPage - 1) * this.props.perPageLimit;
- const filesInPage = (this.doPagination
- ? this.props.allFiles.slice(offset, offset + this.props.perPageLimit)
- : this.props.allFiles
- );
- const allFilesTotal = this.props.allFiles.length;
- return (
- <div>
- <h3>{this.props.txt('{0} ignore file locations', allFilesTotal)}</h3>
- {this.doPagination && <Pagination
- totalRecords={allFilesTotal}
- pageLimit={this.props.perPageLimit}
- pageNeighbours={2}
- onPageChanged={this.onPageChanged}
- refresh={this.state.refreshPagination}
- txt={this.props.txt}
- />}
- <table className="tables ignored-list-table">
- <thead><tr><th>{this.props.txt('Path')}</th></tr></thead>
- <tbody>
- {filesInPage.map((file, index) => {
- return <tr key={file}><td>{file}</td></tr>
- })}
- </tbody>
- </table>
- </div>
- );
- }
- }
- class PreloadedFiles extends React.Component {
- constructor(props) {
- super(props);
- this.doPagination = (typeof props.perPageLimit === "number"
- && props.perPageLimit > 0
- );
- this.state = {
- currentPage: 1,
- refreshPagination: 0
- }
- }
- onPageChanged = currentPage => {
- this.setState({ currentPage });
- }
- render() {
- if (!this.props.allow.fileList) {
- return null;
- }
- if (this.props.allFiles.length === 0) {
- return <p>{this.props.txt('No files have been preloaded <i>opcache.preload</i>')}</p>;
- }
- const { currentPage } = this.state;
- const offset = (currentPage - 1) * this.props.perPageLimit;
- const filesInPage = (this.doPagination
- ? this.props.allFiles.slice(offset, offset + this.props.perPageLimit)
- : this.props.allFiles
- );
- const allFilesTotal = this.props.allFiles.length;
- return (
- <div>
- <h3>{this.props.txt('{0} preloaded files', allFilesTotal)}</h3>
- {this.doPagination && <Pagination
- totalRecords={allFilesTotal}
- pageLimit={this.props.perPageLimit}
- pageNeighbours={2}
- onPageChanged={this.onPageChanged}
- refresh={this.state.refreshPagination}
- txt={this.props.txt}
- />}
- <table className="tables preload-list-table">
- <thead><tr><th>{this.props.txt('Path')}</th></tr></thead>
- <tbody>
- {filesInPage.map((file, index) => {
- return <tr key={file}><td>{file}</td></tr>
- })}
- </tbody>
- </table>
- </div>
- );
- }
- }
- class Pagination extends React.Component {
- constructor(props) {
- super(props);
- this.state = { currentPage: 1 };
- this.pageNeighbours =
- typeof props.pageNeighbours === "number"
- ? Math.max(0, Math.min(props.pageNeighbours, 2))
- : 0;
- }
- componentDidMount() {
- this.gotoPage(1);
- }
- componentDidUpdate(props) {
- const { refresh } = this.props;
- if (props.refresh !== refresh) {
- this.gotoPage(1);
- }
- }
- gotoPage = page => {
- const { onPageChanged = f => f } = this.props;
- const currentPage = Math.max(0, Math.min(page, this.totalPages()));
- this.setState({ currentPage }, () => onPageChanged(currentPage));
- };
- totalPages = () => {
- return Math.ceil(this.props.totalRecords / this.props.pageLimit);
- }
- handleClick = (page, evt) => {
- evt.preventDefault();
- this.gotoPage(page);
- };
- handleJumpLeft = evt => {
- evt.preventDefault();
- this.gotoPage(this.state.currentPage - this.pageNeighbours * 2 - 1);
- };
- handleJumpRight = evt => {
- evt.preventDefault();
- this.gotoPage(this.state.currentPage + this.pageNeighbours * 2 + 1);
- };
- handleMoveLeft = evt => {
- evt.preventDefault();
- this.gotoPage(this.state.currentPage - 1);
- };
- handleMoveRight = evt => {
- evt.preventDefault();
- this.gotoPage(this.state.currentPage + 1);
- };
- range = (from, to, step = 1) => {
- let i = from;
- const range = [];
- while (i <= to) {
- range.push(i);
- i += step;
- }
- return range;
- }
- fetchPageNumbers = () => {
- const totalPages = this.totalPages();
- const pageNeighbours = this.pageNeighbours;
- const totalNumbers = this.pageNeighbours * 2 + 3;
- const totalBlocks = totalNumbers + 2;
- if (totalPages > totalBlocks) {
- let pages = [];
- const leftBound = this.state.currentPage - pageNeighbours;
- const rightBound = this.state.currentPage + pageNeighbours;
- const beforeLastPage = totalPages - 1;
- const startPage = leftBound > 2 ? leftBound : 2;
- const endPage = rightBound < beforeLastPage ? rightBound : beforeLastPage;
- pages = this.range(startPage, endPage);
- const pagesCount = pages.length;
- const singleSpillOffset = totalNumbers - pagesCount - 1;
- const leftSpill = startPage > 2;
- const rightSpill = endPage < beforeLastPage;
- const leftSpillPage = "LEFT";
- const rightSpillPage = "RIGHT";
- if (leftSpill && !rightSpill) {
- const extraPages = this.range(startPage - singleSpillOffset, startPage - 1);
- pages = [leftSpillPage, ...extraPages, ...pages];
- } else if (!leftSpill && rightSpill) {
- const extraPages = this.range(endPage + 1, endPage + singleSpillOffset);
- pages = [...pages, ...extraPages, rightSpillPage];
- } else if (leftSpill && rightSpill) {
- pages = [leftSpillPage, ...pages, rightSpillPage];
- }
- return [1, ...pages, totalPages];
- }
- return this.range(1, totalPages);
- };
- render() {
- if (!this.props.totalRecords || this.totalPages() === 1) {
- return null
- }
- const { currentPage } = this.state;
- const pages = this.fetchPageNumbers();
- return (
- <nav aria-label="File list pagination">
- <ul className="pagination">
- {pages.map((page, index) => {
- if (page === "LEFT") {
- return (
- <React.Fragment key={index}>
- <li className="page-item arrow">
- <a className="page-link" href="#" aria-label={this.props.txt('Previous')} onClick={this.handleJumpLeft}>
- <span aria-hidden="true">↞</span>
- <span className="sr-only">{this.props.txt('Jump back')}</span>
- </a>
- </li>
- <li className="page-item arrow">
- <a className="page-link" href="#" aria-label={this.props.txt('Previous')} onClick={this.handleMoveLeft}>
- <span aria-hidden="true">⇠</span>
- <span className="sr-only">{this.props.txt('Previous page')}</span>
- </a>
- </li>
- </React.Fragment>
- );
- }
- if (page === "RIGHT") {
- return (
- <React.Fragment key={index}>
- <li className="page-item arrow">
- <a className="page-link" href="#" aria-label={this.props.txt('Next')} onClick={this.handleMoveRight}>
- <span aria-hidden="true">⇢</span>
- <span className="sr-only">{this.props.txt('Next page')}</span>
- </a>
- </li>
- <li className="page-item arrow">
- <a className="page-link" href="#" aria-label={this.props.txt('Next')} onClick={this.handleJumpRight}>
- <span aria-hidden="true">↠</span>
- <span className="sr-only">{this.props.txt('Jump forward')}</span>
- </a>
- </li>
- </React.Fragment>
- );
- }
- return (
- <li key={index} className="page-item">
- <a className={`page-link${currentPage === page ? " active" : ""}`} href="#" onClick={e => this.handleClick(page, e)}>
- {page}
- </a>
- </li>
- );
- })}
- </ul>
- </nav>
- );
- }
- }
- function Footer(props) {
- return (
- <footer className="main-footer">
- <a className="github-link" href="https://github.com/amnuts/opcache-gui"
- target="_blank"
- title={props.txt("opcache-gui (currently version {0}) on GitHub", props.version)}
- >https://github.com/amnuts/opcache-gui - {props.txt("version {0}", props.version)}</a>
- <a className="sponsor-link" href="https://github.com/sponsors/amnuts"
- target="_blank"
- title={props.txt("Sponsor this project and author on GitHub")}
- >{props.txt("Sponsor this project")}</a>
- </footer>
- );
- }
- function debounce(func, wait, immediate) {
- let timeout;
- wait = wait || 250;
- return function() {
- let context = this, args = arguments;
- let later = function() {
- timeout = null;
- if (!immediate) {
- func.apply(context, args);
- }
- };
- let callNow = immediate && !timeout;
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) {
- func.apply(context, args);
- }
- };
- }
|