Added sorting and graph to stats

main
Štěpán Škorpil 2022-01-15 20:44:37 +01:00
rodzic 633ab39739
commit 73811f9e2e
8 zmienionych plików z 264 dodań i 75 usunięć

Wyświetl plik

@ -1,24 +1,44 @@
import React, { ReactNode } from 'react'
const Loader:React.FC<{ children: ReactNode, loading: boolean, hideContent?:boolean }> = ({ hideContent, children, loading }) => {
return (
<div className={'loader' + (loading ? ' -loading' : '')}>
<div className={'loader-content'}>
{ hideContent && loading ? '' : children}
</div>
<div className={'loader-visualisation'}>
{loading
const Loader:React.FC<{ children: ReactNode, loading: boolean, hideContent?:boolean, table?:number, showTop?:boolean, showBottom?:boolean }> = ({ showTop, showBottom, hideContent, children, table, loading }) => {
const className = 'loader' + (loading ? ' -loading' : '')
const loaderVisual = (
<div className={'loader-visualisation'}>
<svg xmlns="http://www.w3.org/2000/svg" className='loader-graphics' width="34" height="34">
<path className="rail" d="m 16.977523,0.24095147 c -9.2629169,0 -16.73280045,7.51449143 -16.73280045,16.77740253 0,9.262912 7.46988355,16.777403 16.73280045,16.777403 9.262917,0 16.777413,-7.514491 16.777413,-16.777403 0,-9.2629111 -7.514496,-16.77740253 -16.777413,-16.77740253 z m 0,4.14972823 c 6.966927,0 12.627682,5.6607523 12.627682,12.6276743 0,6.966923 -5.660755,12.583053 -12.627682,12.583053 -6.966937,0 -12.5830596,-5.61613 -12.5830596,-12.583053 0,-6.966922 5.6161226,-12.6276743 12.5830596,-12.6276743 z" />
<path className="train" d="M 31.677259,17.003529 A 14.680208,14.680199 0 0 1 20.796571,31.183505" />
</svg>
<span>Loading...</span>
</div>
)
if (table) {
return (
<>
{showTop && loading
? (
<>
<svg xmlns="http://www.w3.org/2000/svg" className='loader-graphics' width="34" height="34">
<path className="rail" d="m 16.977523,0.24095147 c -9.2629169,0 -16.73280045,7.51449143 -16.73280045,16.77740253 0,9.262912 7.46988355,16.777403 16.73280045,16.777403 9.262917,0 16.777413,-7.514491 16.777413,-16.777403 0,-9.2629111 -7.514496,-16.77740253 -16.777413,-16.77740253 z m 0,4.14972823 c 6.966927,0 12.627682,5.6607523 12.627682,12.6276743 0,6.966923 -5.660755,12.583053 -12.627682,12.583053 -6.966937,0 -12.5830596,-5.61613 -12.5830596,-12.583053 0,-6.966922 5.6161226,-12.6276743 12.5830596,-12.6276743 z" />
<path className="train" d="M 31.677259,17.003529 A 14.680208,14.680199 0 0 1 20.796571,31.183505" />
</svg>
<span>Loading...</span>
</>
<tr className={className}>
<td colSpan={table}>{loaderVisual}</td>
</tr>
)
: ''}
{ hideContent && loading ? '' : children}
{showBottom && loading
? (
<tr className={className}>
<td colSpan={table}>{loaderVisual}</td>
</tr>
)
: ''}
</>
)
}
return (
<div className={className}>
{showTop && loading ? loaderVisual : ''}
<div className={'loader-content'}>
{hideContent && loading ? '' : children}
</div>
{showBottom && loading ? loaderVisual : ''}
</div>
)
}

Wyświetl plik

@ -0,0 +1,14 @@
import React from 'react'
const ProgressBar: React.FC<{ percents: number, way?: 'left' | 'right' | 'top' | 'bottom', color?:string }> = ({ percents, way, color }) => {
way = way ?? 'right'
percents = Math.round(percents)
color = color ?? 'var(--accent-color)'
return (
<div className={'progressbar'} style={{
background: `linear-gradient(to ${way}, ${color} ${percents}%, transparent ${percents}%`
}}/>
)
}
export default ProgressBar

Wyświetl plik

@ -0,0 +1,38 @@
import React from 'react'
import { StatsRequest, StatsRequestSortBy } from '../types/StatsRequest'
const SortToggle: React.FC<{
onToggle:(StatsRequestSortBy)=>void,
field:StatsRequestSortBy,
sort: StatsRequest
}> = ({ onToggle, field, sort, children }) => {
return (
<a className={'sort-toggle'} href={'#'} onClick={() => onToggle(field)}>
<span>{children}</span>
{sort.sortBy === field && sort.sortWay === 'asc'
? (
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sort-up"
className="sort-icon svg-inline--fa fa-sort-up fa-w-10" role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512">
<path fill="currentColor"
d="M279 224H41c-21.4 0-32.1-25.9-17-41L143 64c9.4-9.4 24.6-9.4 33.9 0l119 119c15.2 15.1 4.5 41-16.9 41z"/>
</svg>
)
: ''
}
{sort.sortBy === field && sort.sortWay === 'desc'
? (
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sort-down"
className="sort-icon svg-inline--fa fa-sort-down fa-w-10" role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512">
<path fill="currentColor"
d="M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41z"/>
</svg>
)
: ''
}
</a>
)
}
export default SortToggle

Wyświetl plik

@ -2,6 +2,7 @@ import prisma from '../../lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { StatsResponse, StatsResponseSoftware } from '../../types/StatsResponse'
import { cache } from '../../lib/cache'
import { statsRequestSchema } from '../../types/StatsRequest'
interface StatsItem {
softwarename: string | null,
@ -14,9 +15,13 @@ interface StatsItem {
const CACHE_KEY = 'stats'
const handleGetStats = async (req: NextApiRequest, res: NextApiResponse<StatsResponse>): Promise<void> => {
if (!cache.has(CACHE_KEY)) {
console.info('Retrieving new stats')
const data = await prisma.$queryRaw<StatsItem[]>`
const query = await statsRequestSchema.parseAsync(req.query)
query.sortBy = query.sortBy ?? 'nodeCount'
query.sortWay = query.sortWay ?? 'desc'
const cacheKey = `${CACHE_KEY}_${query.sortWay}_${query.sortBy}`
if (!cache.has(cacheKey)) {
console.info('Retrieving new stats', { cacheKey, query })
const data = await prisma.$queryRawUnsafe<StatsItem[]>(`
select n."softwareName" as softwarename,
count(n.id) as nodecount,
(
@ -40,9 +45,9 @@ const handleGetStats = async (req: NextApiRequest, res: NextApiResponse<StatsRes
from "Node" n
group by n."softwareName"
having count(n.id) > 1
order by nodecount desc;
`
cache.set<StatsResponse>(CACHE_KEY, {
order by ${query.sortBy.toLowerCase() + ' ' + query.sortWay.toUpperCase()};
`)
cache.set<StatsResponse>(cacheKey, {
softwares: data.map(
(item: StatsItem): StatsResponseSoftware => {
return {
@ -56,9 +61,9 @@ const handleGetStats = async (req: NextApiRequest, res: NextApiResponse<StatsRes
)
}, parseInt(process.env.STATS_CACHE_MINUTES ?? '60') * 60 * 1000)
}
console.info('Returning stats from cache')
console.info('Returning stats from cache', { cacheKey, query })
res.status(200)
.json(cache.get<StatsResponse>(CACHE_KEY))
.json(cache.get<StatsResponse>(cacheKey))
}
export default handleGetStats

Wyświetl plik

@ -131,7 +131,7 @@ const Home:React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = ({
<span>Search</span>
</button>
</form>
<Loader loading={loading}>
<Loader loading={loading} showBottom={true}>
{
loaded
? <Results feeds={results}/>

Wyświetl plik

@ -7,82 +7,164 @@ import Loader from '../components/Loader'
import axios from 'axios'
import { StatsResponse, statsResponseSchema } from '../types/StatsResponse'
import SoftwareBadge from '../components/badges/SoftwareBadge'
import ProgressBar from '../components/ProgressBar'
import { StatsRequest, StatsRequestSortBy } from '../types/StatsRequest'
import SortToggle from '../components/SortToggle'
let source = axios.CancelToken.source()
const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = ({ matomoConfig }) => {
const [loading, setLoading] = useState<boolean>(true)
const [stats, setStats] = useState<StatsResponse|null>(null)
const [loaded, setLoaded] = useState<boolean>(false)
const [stats, setStats] = useState<StatsResponse | null>(null)
const [sort, setSort] = useState<StatsRequest>({
sortBy: 'nodeCount', sortWay: 'desc'
})
const loadStats = async () => {
const toggleSort = (sortBy: StatsRequestSortBy) => {
setSort({
sortBy: sortBy,
sortWay: sort.sortBy === sortBy && sort.sortWay === 'asc' ? 'desc' : 'asc'
})
}
const retrieveStats = async () => {
console.info('Retrieving stats', { sort })
source = axios.CancelToken.source()
setLoading(true)
try {
const response = await axios.get('/api/stats')
const response = await axios.get('/api/stats', {
params: sort,
cancelToken: source.token
})
const stats = await statsResponseSchema.parseAsync(response.data)
setStats(stats)
} catch (err) {
setStats(null)
console.log(err)
}
setLoaded(true)
setLoading(false)
}
const loadStats = async () => {
console.info('Cancelling retrivals')
source.cancel('New query on the way')
setTimeout(retrieveStats)
}
useEffect(() => {
loadStats()
}, [])
const summary = {
}, [sort])
const sum = {
nodeCount: 0,
accountCount: 0,
channelCount: 0
}
const max = {
nodeCount: 0,
accountCount: 0,
channelCount: 0
}
const softwares = stats === null ? [] : stats.softwares
softwares.forEach(software => {
if (software.name === null) {
return
}
sum.nodeCount += software.nodeCount
sum.accountCount += software.accountCount
sum.channelCount += software.channelCount
max.nodeCount = Math.max(software.nodeCount, max.nodeCount)
max.accountCount = Math.max(software.accountCount, max.accountCount)
max.channelCount = Math.max(software.channelCount, max.channelCount)
})
return (
<Layout matomoConfig={matomoConfig}>
<Head>
<title>{'Stats | ' + siteTitle}</title>
</Head>
<h1>Index stats</h1>
<Loader loading={loading} hideContent={true}>
{stats === null
? (<p>Failed to load stats data!</p>)
: (
<>
<table>
<thead>
<tr>
<th>Software</th>
<th className={'number-cell'}>Instance count</th>
<th className={'number-cell'}>Account count</th>
<th className={'number-cell'}>Channel count</th>
</tr>
</thead>
<tbody>
{
stats.softwares.map((software, index) => {
summary.nodeCount += software.nodeCount
summary.accountCount += software.accountCount
summary.channelCount += software.channelCount
return (
<tr key={index}>
<td>{software.name !== null
? <SoftwareBadge softwareName={software.name}/>
: <em>Not recognized</em>}</td>
<td className={'number-cell'}>{software.nodeCount}</td>
<td className={'number-cell'}>{software.accountCount}</td>
<td className={'number-cell'}>{software.channelCount}</td>
</tr>
)
})
}
</tbody>
<tfoot>
<tr>
<th>Summary</th>
<th className={'number-cell'}>{summary.nodeCount}</th>
<th className={'number-cell'}>{summary.accountCount}</th>
<th className={'number-cell'}>{summary.channelCount}</th>
</tr>
</tfoot>
</table>
</>
)
}
</Loader>
<table>
<thead>
<tr>
<th>
<SortToggle onToggle={toggleSort} field={'softwareName'} sort={sort}>
Software name
</SortToggle>
</th>
<th className={'number-cell'}>
<SortToggle onToggle={toggleSort} field={'nodeCount'} sort={sort}>
Instance count
</SortToggle>
</th>
<th className={'number-cell'}>
<SortToggle onToggle={toggleSort} field={'accountCount'} sort={sort}>
Account count
</SortToggle>
</th>
<th className={'number-cell'}>
<SortToggle onToggle={toggleSort} field={'channelCount'} sort={sort}>
Channel count
</SortToggle>
</th>
</tr>
</thead>
<Loader loading={loading} hideContent={!loaded} table={4} showTop={true}>
{stats === null
? (<tr><td colSpan={4}><em>Failed to load stats data!</em></td></tr>)
: (
<>
<tbody>
{
stats.softwares.map((software, index) => {
return software.name !== null
? (
<tr key={index}>
<td>
<SoftwareBadge softwareName={software.name}/>
</td>
<td className={'number-cell'}>
<span>{software.nodeCount}</span>
<ProgressBar way={'left'}
percents={100 * software.nodeCount / max.nodeCount}/>
</td>
<td className={'number-cell'}>
<span>{software.accountCount}</span>
<ProgressBar way={'left'}
percents={100 * software.accountCount / max.accountCount}/>
</td>
<td className={'number-cell'}>
<span>{software.channelCount}</span>
<ProgressBar way={'left'}
percents={100 * software.channelCount / max.channelCount}/>
</td>
</tr>
)
: (
<tr key={index}>
<td><em>Not recognized</em></td>
<td className={'number-cell'}><span>{software.nodeCount}</span></td>
<td className={'number-cell'}><span>{software.accountCount}</span>
</td>
<td className={'number-cell'}><span>{software.channelCount}</span>
</td>
</tr>
)
})
}
</tbody>
<tfoot>
<tr>
<th>Summary</th>
<th className={'number-cell'}>{sum.nodeCount}</th>
<th className={'number-cell'}>{sum.accountCount}</th>
<th className={'number-cell'}>{sum.channelCount}</th>
</tr>
</tfoot>
</>
)
}
</Loader>
</table>
</Layout>
)
}

Wyświetl plik

@ -34,6 +34,22 @@ table {
&.number-cell {
text-align: right;
}
.progressbar {
background-color: var(--accent-color);
height: 2px;
width: 100%;
border-radius: 1px;
}
.sort-toggle{
svg{
max-width: 1em;
max-height: 1em;
margin-left: 0.5em;
vertical-align: baseline;
}
}
}
th {
@ -238,6 +254,7 @@ nav {
&.active {
font-weight: bold;
color: var(--main-fg-color);
svg {
max-width: 1.5em;
max-height: 1.5em;
@ -253,6 +270,8 @@ nav {
}
.loader {
text-align: center;
.loader-visualisation {
margin: 1em 0;

Wyświetl plik

@ -0,0 +1,11 @@
import { z } from 'zod'
export const statsRequestSortBySchema = z.enum(['nodeCount', 'accountCount', 'channelCount', 'softwareName'])
export const statsRequestSortWaySchema = z.enum(['asc', 'desc'])
export const statsRequestSchema = z.object({
sortBy: z.optional(statsRequestSortBySchema),
sortWay: z.optional(statsRequestSortWaySchema)
})
export type StatsRequest = z.infer<typeof statsRequestSchema>
export type StatsRequestSortWay = z.infer<typeof statsRequestSortWaySchema>
export type StatsRequestSortBy = z.infer<typeof statsRequestSortBySchema>