mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-06-22 04:58:40 +00:00
Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-cm-features
This commit is contained in:
@@ -111,6 +111,10 @@ body {
|
|||||||
vertical-align : middle;
|
vertical-align : middle;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
border-right : 1px solid;
|
border-right : 1px solid;
|
||||||
|
max-width:50ch;
|
||||||
|
overflow:hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&:last-child { border-right : none; }
|
&:last-child { border-right : none; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,70 +1,174 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import request from 'superagent';
|
import request from 'superagent';
|
||||||
|
import Moment from 'moment';
|
||||||
|
|
||||||
const BrewCleanup = ({})=>{
|
const BrewCleanup = ({})=>{
|
||||||
const [count, setCount] = useState(0);
|
const [junkBrewCollection, setJunkBrewCollection] = useState([]);
|
||||||
const [pending, setPending] = useState(false);
|
const [lostBrewCollection, setLostBrewCollection] = useState([]);
|
||||||
const [primed, setPrimed] = useState(false);
|
const [pendingJunk, setPendingJunk] = useState(false);
|
||||||
|
const [pendingLost, setPendingLost] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
const prime = async ()=>{
|
const find = async (type)=>{
|
||||||
setPending(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await request.get('/admin/cleanup');
|
|
||||||
|
|
||||||
setCount(res.body.count);
|
if(type === 'junk') try {
|
||||||
setPrimed(true);
|
setPendingJunk(true);
|
||||||
|
const res = await request.get('/admin/cleanupJunk');
|
||||||
|
|
||||||
|
setJunkBrewCollection(res.body.brewCollection);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err);
|
setError(err);
|
||||||
} finally {
|
} finally {
|
||||||
setPending(false);
|
setPendingJunk(false);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const cleanup = async ()=>{
|
if(type === 'lost') try {
|
||||||
setPending(true);
|
setPendingLost(true);
|
||||||
|
const res = await request.get('/admin/cleanupLost');
|
||||||
|
|
||||||
try {
|
setLostBrewCollection(res.body.brewCollection);
|
||||||
const res = await request.post('/admin/cleanup');
|
|
||||||
|
|
||||||
setCount(res.body.count);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err);
|
setError(err);
|
||||||
} finally {
|
} finally {
|
||||||
setPending(false);
|
setPendingLost(false);
|
||||||
setPrimed(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const renderPrimed = ()=>{
|
|
||||||
if(!primed) return;
|
|
||||||
|
|
||||||
if(!count) return <div className='result noBrews'>No Matching Brews found.</div>;
|
const cleanup = async (type)=>{
|
||||||
|
|
||||||
|
if(type === 'junk') try {
|
||||||
|
setPendingJunk(true);
|
||||||
|
console.log('deleting junk');
|
||||||
|
const res = await request.post('/admin/cleanupJunk');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setPendingJunk(false);
|
||||||
|
setJunkBrewCollection([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type === 'lost') try {
|
||||||
|
setPendingLost(true);
|
||||||
|
const res = await request.post('/admin/cleanupLost');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setPendingLost(false);
|
||||||
|
setLostBrewCollection([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBrewList = (type)=>{
|
||||||
|
const brewList = type === 'lost' ? lostBrewCollection : junkBrewCollection;
|
||||||
|
|
||||||
|
if(!brewList || brewList.length === 0) {
|
||||||
|
return <>
|
||||||
|
<h3>{`Results - No brews found` }</h3>
|
||||||
|
<table className='resultsTable'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Last Update</th>
|
||||||
|
<th>last viewed</th>
|
||||||
|
<th>Storage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colSpan={4}><strong>"No brews found"</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
console.log(type);
|
||||||
|
console.log(brewList);
|
||||||
|
return <>
|
||||||
|
<h3>{`Results - ${brewList.length} brews` }</h3>
|
||||||
|
<table className='resultsTable'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Last Update</th>
|
||||||
|
<th>last viewed</th>
|
||||||
|
<th>Storage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{brewList
|
||||||
|
.sort((a, b)=>{ // Sort brews from most recently updated
|
||||||
|
if(a.lastViewed > b.lastViewed) return -1;
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.map((brew, idx)=>{
|
||||||
|
return <tr key={idx}>
|
||||||
|
<td><strong>{brew.title || 'No Title'}</strong></td>
|
||||||
|
<td style={{ width: '200px' }}>{Moment(brew.updatedAt).fromNow()}</td>
|
||||||
|
<td>{brew.lastViewed ? Moment(brew.lastViewed).fromNow() : 'No last viewed date'}</td>
|
||||||
|
<td>{brew.googleId ? 'Google' : 'Homebrewery'}</td>
|
||||||
|
</tr>
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
const renderFound = (type)=>{
|
||||||
|
const deleteButton = !(type === 'junk' && junkBrewCollection.length === 0 || type === 'lost' && lostBrewCollection.length === 0);
|
||||||
|
|
||||||
return <div className='result'>
|
return <div className='result'>
|
||||||
<button onClick={()=>cleanup()} className='remove'>
|
{deleteButton && <button onClick={()=>cleanup(type)} className='remove'>
|
||||||
{pending
|
{pendingLost && type === "lost" || pendingJunk && type === "junk"
|
||||||
? <i className='fas fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
: <span><i className='fas fa-times' /> Remove</span>
|
: <span><i className='fas fa-times' /> Remove</span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
<span>Found {count} Brews that could be removed. </span>
|
}
|
||||||
|
{renderBrewList(type)}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
const renderJunkBrewCleanup = ()=>{
|
||||||
|
return <div className='junk'>
|
||||||
|
<h3> Junk brews</h3>
|
||||||
|
<p>Queries unauthored brews that have not been viewed or <br/>updated in 30 days and are shorter than 140 bytes (up to 300)</p>
|
||||||
|
|
||||||
|
<button onClick={()=>find('junk')} className='query'>
|
||||||
|
{pendingJunk
|
||||||
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
|
: 'Query Brews'
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
{renderFound('junk')}
|
||||||
|
|
||||||
|
{error && <div className='error noBrews'>{error.toString()}</div>}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
const renderLostBrewCleanup = ()=>{
|
||||||
|
return <div className='lost'>
|
||||||
|
<h3> Lost brews</h3>
|
||||||
|
<p>Queries unauthored brews that have not been <br/>updated or viewed for 2 years (up to 500)</p>
|
||||||
|
|
||||||
|
<button onClick={()=>find('lost')} className='query'>
|
||||||
|
{pendingLost
|
||||||
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
|
: 'Query Brews'
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
{renderFound('lost')}
|
||||||
|
|
||||||
|
{error && <div className='error noBrews'>{error.toString()}</div>}
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className='brewUtil brewCleanup'>
|
return <div className='brewUtil brewCleanup'>
|
||||||
<h2> Brew Cleanup </h2>
|
<h2> Brew Cleanup </h2>
|
||||||
<p>Removes very short brews to tidy up the database</p>
|
{renderJunkBrewCleanup()}
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
{renderLostBrewCleanup()}
|
||||||
|
|
||||||
<button onClick={()=>prime()} className='query'>
|
|
||||||
{pending
|
|
||||||
? <i className='fas fa-spin fa-spinner' />
|
|
||||||
: 'Query Brews'
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
{renderPrimed()}
|
|
||||||
|
|
||||||
{error && <div className='error noBrews'>{error.toString()}</div>}
|
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ const SplitPane = (props)=>{
|
|||||||
const [liveScroll, setLiveScroll] = useState(false);
|
const [liveScroll, setLiveScroll] = useState(false);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY);
|
handleResize();
|
||||||
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
|
|
||||||
setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true');
|
setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true');
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
@@ -29,7 +28,10 @@ const SplitPane = (props)=>{
|
|||||||
const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
|
const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
|
||||||
|
|
||||||
//when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
|
//when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
|
||||||
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(PANE_WIDTH_KEY), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
|
const handleResize = ()=>{
|
||||||
|
const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY);
|
||||||
|
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
|
||||||
|
};
|
||||||
|
|
||||||
const handleUp =(e)=>{
|
const handleUp =(e)=>{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
+47
-10
@@ -39,14 +39,29 @@ export default function createAdminApi(vite) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const junkBrewPipeline = [
|
// Search for up to 300 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
||||||
|
const junkBrewsPipeline = [
|
||||||
{ $match : {
|
{ $match : {
|
||||||
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
|
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
|
||||||
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
|
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
|
||||||
} },
|
} },
|
||||||
{ $project: { textBinSize: { $binarySize: '$textBin' } } },
|
{ $project: { _id: 1, textBinSize: { $binarySize: '$textBin' }, updatedAt: 1, lastViewed: 1} },
|
||||||
{ $match: { textBinSize: { $lt: 140 } } },
|
{ $match: { textBinSize: { $lt: 140 } } },
|
||||||
{ $limit: 100 }
|
{ $limit: 300 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Search for up to 500 unauthored brews that have not been viewed or updated in two years
|
||||||
|
const lostBrewsPipeline = [
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
authors: [],
|
||||||
|
updatedAt: { $lt: Moment().subtract(2, 'years').toDate() },
|
||||||
|
lastViewed: { $lt: Moment().subtract(2, 'years').toDate() }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$limit: 500
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
/* Search for brews that aren't compressed (missing the compressed text field) */
|
/* Search for brews that aren't compressed (missing the compressed text field) */
|
||||||
@@ -54,19 +69,41 @@ export default function createAdminApi(vite) {
|
|||||||
'text' : { '$exists': true }
|
'text' : { '$exists': true }
|
||||||
}).lean().limit(10000).select('_id');
|
}).lean().limit(10000).select('_id');
|
||||||
|
|
||||||
// Search for up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
router.get('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{
|
||||||
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||||
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
|
.then((objs)=>res.json({ count: objs.length, brewCollection : objs }))
|
||||||
.then((objs)=>res.json({ count: objs.length }))
|
|
||||||
.catch((error)=>{
|
.catch((error)=>{
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.status(500).json({ error: 'Internal Server Error' });
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
// Delete result of junkBrewsPipeline
|
||||||
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
router.post('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{
|
||||||
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
|
HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||||
|
.then((docs)=>{
|
||||||
|
const ids = docs.map((doc)=>doc._id);
|
||||||
|
return HomebrewModel.deleteMany({ _id: { $in: ids } });
|
||||||
|
}).then((result)=>{
|
||||||
|
res.json({ count: result.deletedCount });
|
||||||
|
}).catch((error)=>{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/admin/cleanupLost', mw.adminOnly, (req, res)=>{
|
||||||
|
HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||||
|
.then((objs)=>res.json({ count: objs.length, brewCollection : objs }))
|
||||||
|
.catch((error)=>{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete result of lostBrewsPipeline
|
||||||
|
router.post('/admin/cleanupLost', mw.adminOnly, (req, res)=>{
|
||||||
|
HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||||
.then((docs)=>{
|
.then((docs)=>{
|
||||||
const ids = docs.map((doc)=>doc._id);
|
const ids = docs.map((doc)=>doc._id);
|
||||||
return HomebrewModel.deleteMany({ _id: { $in: ids } });
|
return HomebrewModel.deleteMany({ _id: { $in: ids } });
|
||||||
|
|||||||
Reference in New Issue
Block a user