You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
387 lines
15 KiB
387 lines
15 KiB
import React from "react" ;
|
|
import { Link } from "react-router-dom" ;
|
|
import { Tabs, TabList, TabPanel, Tab } from 'react-tabs';
|
|
import 'react-tabs/style/react-tabs.css';
|
|
import "./DbReport.css" ;
|
|
import { PreviewableImage } from "./PreviewableImage" ;
|
|
import { gAppRef } from "./App.js" ;
|
|
import { makeCollapsibleList, pluralString, isLink } from "./utils.js" ;
|
|
|
|
const axios = require( "axios" ) ;
|
|
|
|
// --------------------------------------------------------------------
|
|
|
|
export class DbReport extends React.Component
|
|
{
|
|
// render the component
|
|
render() {
|
|
return ( <div id="db-report">
|
|
<div className="section"> <DbRowCounts /> </div>
|
|
<div className="section"> <DbLinks /> </div>
|
|
<div className="section"> <DbImages /> </div>
|
|
</div>
|
|
) ;
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
class DbRowCounts extends React.Component
|
|
{
|
|
|
|
constructor( props ) {
|
|
|
|
// initialize
|
|
super( props ) ;
|
|
this.state = {
|
|
dbRowCounts: null,
|
|
} ;
|
|
|
|
// get the database row counts
|
|
axios.get(
|
|
gAppRef.makeFlaskUrl( "/db-report/row-counts" )
|
|
).then( resp => {
|
|
this.setState( { dbRowCounts: resp.data } ) ;
|
|
} ).catch( err => {
|
|
gAppRef.showErrorResponse( "Can't get the database row counts", err ) ;
|
|
} ) ;
|
|
|
|
}
|
|
|
|
render() {
|
|
|
|
// initialize
|
|
const dbRowCounts = this.state.dbRowCounts ;
|
|
|
|
// render the table rows
|
|
function makeRowCountRow( tableName ) {
|
|
const tableName2 = tableName[0].toUpperCase() + tableName.substring(1) ;
|
|
let nRows ;
|
|
if ( dbRowCounts ) {
|
|
nRows = dbRowCounts[ tableName ] ;
|
|
const nImages = dbRowCounts[ tableName+"_image" ] ;
|
|
if ( nImages > 0 )
|
|
nRows = ( <span>
|
|
{nRows} <span className="images">({pluralString(nImages,"image")})</span>
|
|
</span>
|
|
) ;
|
|
}
|
|
return ( <tr key={tableName}>
|
|
<td style={{paddingRight:"0.5em",fontWeight:"bold"}}> {tableName2}s: </td>
|
|
<td> {nRows} </td>
|
|
</tr>
|
|
) ;
|
|
}
|
|
let tableRows = [ "publisher", "publication", "article", "author", "scenario" ].map(
|
|
(tableName) => makeRowCountRow( tableName )
|
|
) ;
|
|
|
|
// render the component
|
|
return ( <div className="db-row-counts">
|
|
<h2> Content { !dbRowCounts && <img src="/images/loading.gif" className="loading" alt="Loading..." /> } </h2>
|
|
<table><tbody>{tableRows}</tbody></table>
|
|
</div>
|
|
) ;
|
|
}
|
|
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
class DbLinks extends React.Component
|
|
{
|
|
|
|
constructor( props ) {
|
|
|
|
// initialize
|
|
super( props ) ;
|
|
this.state = {
|
|
dbLinks: null,
|
|
linksToCheck: null, currLinkToCheck: null, isFirstLinkCheck: true,
|
|
checkLinksInProgress: false, checkLinksStatusMsg: null,
|
|
linkErrors: {},
|
|
} ;
|
|
|
|
// initialize
|
|
this._getLinksToCheck() ;
|
|
|
|
}
|
|
|
|
render() {
|
|
|
|
// initialize
|
|
const dbLinks = this.state.dbLinks ;
|
|
|
|
// render the table rows
|
|
let tableRows = [] ;
|
|
for ( let key of [ "publisher", "publication", "article" ] ) {
|
|
const nDbLinks = dbLinks && dbLinks[key] ? dbLinks[key].length : null ;
|
|
const key2 = key[0].toUpperCase() + key.substring(1) + "s" ;
|
|
tableRows.push( <tr key={key}>
|
|
<td style={{paddingRight:"0.5em",fontWeight:"bold"}}> {key2}: </td>
|
|
<td style={{width:"100%"}}> {nDbLinks} </td>
|
|
</tr>
|
|
) ;
|
|
if ( this.state.linkErrors[ key ] ) {
|
|
// NOTE: Showing all the errors at once (e.g. not as a collapsible list) will be unwieldy
|
|
// if there are a lot of them, but this shouldn't happen often, and if it does, the user
|
|
// is likely to stop the check, fix the problem, then try again.
|
|
let rows = [] ;
|
|
for ( let linkError of this.state.linkErrors[ key ] ) {
|
|
const url = gAppRef.makeAppUrl( "/" + linkError[0][0] + "/" + linkError[0][1] ) ;
|
|
const targetUrl = linkError[0][3] ;
|
|
const target = isLink( targetUrl )
|
|
? <a href={targetUrl}>{targetUrl}</a>
|
|
: targetUrl ;
|
|
let errorMsg = linkError[1] && linkError[1] + ": " ;
|
|
rows.push( <li key={linkError[0]}>
|
|
<Link to={url} dangerouslySetInnerHTML={{__html:linkError[0][2]}} />
|
|
<span className="status"> ({errorMsg}{target}) </span>
|
|
</li>
|
|
) ;
|
|
}
|
|
tableRows.push( <tr key={key+"-errors"}>
|
|
<td colSpan="2">
|
|
<ul className="link-errors"> {rows} </ul>
|
|
</td>
|
|
</tr>
|
|
) ;
|
|
}
|
|
}
|
|
|
|
// render the component
|
|
const nLinksToCheck = this.state.linksToCheck ? this.state.linksToCheck.length - this.state.currLinkToCheck : null ;
|
|
const imageUrl = this.state.checkLinksInProgress ? "/images/loading.gif" : "/images/check-db-links.png" ;
|
|
return ( <div className="db-links">
|
|
<h2> Links { !dbLinks && <img src="/images/loading.gif" className="loading" alt="Loading..." /> } </h2>
|
|
{ this.state.linksToCheck && this.state.linksToCheck.length > 0 && (
|
|
<div className="check-links-frame">
|
|
<button className="check-links" style={{display:"flex"}} onClick={() => this.checkDbLinks()} >
|
|
<img src={imageUrl} style={{height:"1em",marginTop:"0.15em",marginRight:"0.5em"}} alt="Check database links." />
|
|
{ this.state.checkLinksInProgress ? "Stop checking" : "Check links (" + nLinksToCheck + ")" }
|
|
</button>
|
|
<div className="status-msg"> {this.state.checkLinksStatusMsg} </div>
|
|
</div>
|
|
) }
|
|
<table className="db-links" style={{width:"100%"}}><tbody>{tableRows}</tbody></table>
|
|
</div>
|
|
) ;
|
|
}
|
|
|
|
checkDbLinks() {
|
|
// start/stop checking links
|
|
const inProgress = ! this.state.checkLinksInProgress ;
|
|
this.setState( { checkLinksInProgress: inProgress } ) ;
|
|
if ( inProgress )
|
|
this._checkNextLink() ;
|
|
}
|
|
|
|
_checkNextLink( force ) {
|
|
|
|
// check if this is the start of a new run
|
|
if ( this.state.currLinkToCheck === 0 && !force ) {
|
|
// yup - reset the UI
|
|
this.setState( { linkErrors: {} } ) ;
|
|
// NOTE: If the user is checking the links *again*, it could be because some links were flagged
|
|
// during the first run, they've fixed them up, and want to check everything again. In this case,
|
|
// we need to re-fetch the links from the database.
|
|
if ( ! this.state.isFirstLinkCheck ) {
|
|
this._getLinksToCheck(
|
|
() => { this._checkNextLink( true ) ; },
|
|
() => { this.setState( { checkLinksInProgress: false } ) ; }
|
|
) ;
|
|
return ;
|
|
}
|
|
}
|
|
|
|
// check if this is the end of a run
|
|
if ( this.state.currLinkToCheck >= this.state.linksToCheck.length ) {
|
|
// yup - reset the UI
|
|
this.setState( {
|
|
checkLinksStatusMsg: "Checked " + pluralString( this.state.linksToCheck.length, "link" ) + ".",
|
|
currLinkToCheck: 0, // nb: to allow the user to check again
|
|
checkLinksInProgress: false,
|
|
isFirstLinkCheck: false,
|
|
} ) ;
|
|
return ;
|
|
}
|
|
|
|
// get the next link to check
|
|
const linkToCheck = this.state.linksToCheck[ this.state.currLinkToCheck ] ;
|
|
this.setState( { currLinkToCheck: this.state.currLinkToCheck + 1 } ) ;
|
|
|
|
let continueCheckLinks = () => {
|
|
// update the UI
|
|
this.setState( { checkLinksStatusMsg:
|
|
"Checked " + this.state.currLinkToCheck + " of " + pluralString( this.state.linksToCheck.length, "link" ) + "..."
|
|
} ) ;
|
|
// check the next link
|
|
if ( this.state.checkLinksInProgress )
|
|
this._checkNextLink() ;
|
|
}
|
|
|
|
// check the next link
|
|
let url = linkToCheck[3] ;
|
|
if ( url.substr( 0, 14 ) === "http://{FLASK}" )
|
|
url = gAppRef.makeFlaskUrl( url.substr( 14 ) ) ;
|
|
// NOTE: Because of CORS, we have to proxy URL's that don't belong to us via the backend :-/
|
|
let req = isLink( url )
|
|
? axios.post( gAppRef.makeFlaskUrl( "/db-report/check-link", {url:url} ) )
|
|
: axios.head( gAppRef.makeExternalDocUrl( url ) ) ;
|
|
req.then( resp => {
|
|
// the link worked - continue checking links
|
|
continueCheckLinks() ;
|
|
} ).catch( err => {
|
|
// the link failed - record the error
|
|
let newLinkErrors = this.state.linkErrors ;
|
|
if ( newLinkErrors[ linkToCheck[0] ] === undefined )
|
|
newLinkErrors[ linkToCheck[0] ] = [] ;
|
|
const errorMsg = err.response ? "HTTP " + err.response.status : null ;
|
|
newLinkErrors[ linkToCheck[0] ].push( [ linkToCheck, errorMsg ] ) ;
|
|
this.setState( { linkErrors: newLinkErrors } ) ;
|
|
// continue checking links
|
|
continueCheckLinks() ;
|
|
} ) ;
|
|
|
|
}
|
|
|
|
_getLinksToCheck( onOK, onError ) {
|
|
// get the links in the database
|
|
axios.get(
|
|
gAppRef.makeFlaskUrl( "/db-report/links" )
|
|
).then( resp => {
|
|
const dbLinks = resp.data ;
|
|
// flatten the links to a list
|
|
let linksToCheck = [] ;
|
|
for ( let key of [ "publisher", "publication", "article" ] ) {
|
|
for ( let row of dbLinks[key] ) {
|
|
linksToCheck.push( [
|
|
key, row[0], row[1], row[2]
|
|
] ) ;
|
|
}
|
|
}
|
|
this.setState( {
|
|
dbLinks: resp.data,
|
|
linksToCheck: linksToCheck,
|
|
currLinkToCheck: 0,
|
|
} ) ;
|
|
if ( onOK )
|
|
onOK() ;
|
|
} ).catch( err => {
|
|
gAppRef.showErrorResponse( "Can't get the database links", err ) ;
|
|
if ( onError )
|
|
onError() ;
|
|
} ) ;
|
|
}
|
|
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
class DbImages extends React.Component
|
|
{
|
|
|
|
constructor( props ) {
|
|
|
|
// initialize
|
|
super( props ) ;
|
|
this.state = {
|
|
dbImages: null,
|
|
} ;
|
|
|
|
// get the database images
|
|
axios.get(
|
|
gAppRef.makeFlaskUrl( "/db-report/images" )
|
|
).then( resp => {
|
|
this.setState( { dbImages: resp.data } ) ;
|
|
} ).catch( err => {
|
|
gAppRef.showErrorResponse( "Can't get the database images", err ) ;
|
|
} ) ;
|
|
|
|
}
|
|
|
|
render() {
|
|
|
|
// initialize
|
|
const dbImages = this.state.dbImages ;
|
|
|
|
// render any duplicate images
|
|
let dupeImages = [] ;
|
|
if ( dbImages ) {
|
|
for ( let hash in dbImages.duplicates ) {
|
|
let parents = [] ;
|
|
for ( let row of dbImages.duplicates[hash] ) {
|
|
const url = gAppRef.makeAppUrl( "/" + row[0] + "/" + row[1] ) ;
|
|
parents.push(
|
|
<Link to={url} dangerouslySetInnerHTML={{__html:row[2]}} />
|
|
) ;
|
|
}
|
|
// NOTE: We just use the first row's image since, presumably, they will all be the same.
|
|
const row = dbImages.duplicates[hash][ 0 ] ;
|
|
const imageUrl = gAppRef.makeFlaskImageUrl( row[0], row[1] ) ;
|
|
const caption = ( <span>
|
|
Found a duplicate image <span className="hash">(md5:{hash})</span>
|
|
</span>
|
|
) ;
|
|
dupeImages.push( <div className="dupe-image" style={{display:"flex"}} key={hash} >
|
|
<PreviewableImage url={imageUrl} style={{width:"3em",marginTop:"0.1em",marginRight:"0.5em"}} />
|
|
{ makeCollapsibleList( caption, parents, 5, {flexGrow:1}, hash ) }
|
|
</div>
|
|
) ;
|
|
}
|
|
}
|
|
|
|
// render the image sizes
|
|
let tabList = [] ;
|
|
let tabPanels = [] ;
|
|
if ( dbImages ) {
|
|
function toKB( n ) { return ( n / 1024 ).toFixed( 1 ) ; }
|
|
for ( let key of [ "publisher", "publication", "article" ] ) {
|
|
const tableName2 = key[0].toUpperCase() + key.substring(1) ;
|
|
tabList.push(
|
|
<Tab key={key}> {tableName2+"s"} </Tab>
|
|
) ;
|
|
let rows = [] ;
|
|
for ( let row of dbImages[key] ) {
|
|
const url = gAppRef.makeAppUrl( "/" + key + "/" + row[1] ) ;
|
|
// NOTE: Loading every image will be expensive, but we assume we're talking to a local server.
|
|
// Otherwise, we could use a generic "preview" image, and expand it out to the real image
|
|
// when the user clicks on it.
|
|
const imageUrl = gAppRef.makeFlaskImageUrl( key, row[1] ) ;
|
|
rows.push( <tr key={row}>
|
|
<td> <PreviewableImage url={imageUrl} /> </td>
|
|
<td> {toKB(row[0])} </td>
|
|
<td> <Link to={url} dangerouslySetInnerHTML={{__html:row[2]}} /> </td>
|
|
</tr>
|
|
) ;
|
|
}
|
|
tabPanels.push( <TabPanel key={key}>
|
|
{ rows.length === 0 ? "No images found." :
|
|
<table className="image-sizes"><tbody>
|
|
<tr><th style={{width:"1.25em"}}/><th style={{paddingRight:"0.5em"}}> Size (KB) </th><th> {tableName2} </th></tr>
|
|
{rows}
|
|
</tbody></table>
|
|
}
|
|
</TabPanel>
|
|
) ;
|
|
}
|
|
}
|
|
const imageSizes = tabList.length > 0 && ( <Tabs>
|
|
<TabList> {tabList} </TabList>
|
|
{tabPanels}
|
|
</Tabs>
|
|
) ;
|
|
|
|
// render the component
|
|
return ( <div className="db-images">
|
|
<h2> Images { !dbImages && <img src="/images/loading.gif" className="loading" alt="Loading..." /> } </h2>
|
|
{ dupeImages.length > 0 &&
|
|
<div className="dupe-analysis"> {dupeImages} </div>
|
|
}
|
|
{imageSizes}
|
|
</div>
|
|
) ;
|
|
|
|
}
|
|
|
|
}
|
|
|