Manage ASL magazines and their articles.
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.
 
 
 
 
 
asl-articles/web/src/DbReport.js

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>
) ;
}
}