diff --git a/application/src/components/Loader.tsx b/application/src/components/Loader.tsx index b166b5c..c583532 100644 --- a/application/src/components/Loader.tsx +++ b/application/src/components/Loader.tsx @@ -1,24 +1,44 @@ import React, { ReactNode } from 'react' -const Loader:React.FC<{ children: ReactNode, loading: boolean, hideContent?:boolean }> = ({ hideContent, children, loading }) => { - return ( -
-
- { hideContent && loading ? '' : children} -
-
- {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 = ( +
+ + + + + Loading... +
+ ) + if (table) { + return ( + <> + {showTop && loading ? ( - <> - - - - - Loading... - + + {loaderVisual} + ) : ''} + { hideContent && loading ? '' : children} + {showBottom && loading + ? ( + + {loaderVisual} + + ) + : ''} + + ) + } + return ( +
+ {showTop && loading ? loaderVisual : ''} +
+ {hideContent && loading ? '' : children}
+ {showBottom && loading ? loaderVisual : ''}
) } diff --git a/application/src/components/ProgressBar.tsx b/application/src/components/ProgressBar.tsx new file mode 100644 index 0000000..ed22bea --- /dev/null +++ b/application/src/components/ProgressBar.tsx @@ -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 ( +
+ ) +} + +export default ProgressBar diff --git a/application/src/components/SortToggle.tsx b/application/src/components/SortToggle.tsx new file mode 100644 index 0000000..9b57384 --- /dev/null +++ b/application/src/components/SortToggle.tsx @@ -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 ( + onToggle(field)}> + {children} + {sort.sortBy === field && sort.sortWay === 'asc' + ? ( + + ) + : '' + } + {sort.sortBy === field && sort.sortWay === 'desc' + ? ( + + ) + : '' + } + + ) +} + +export default SortToggle diff --git a/application/src/pages/api/stats.ts b/application/src/pages/api/stats.ts index a4393c3..a353418 100644 --- a/application/src/pages/api/stats.ts +++ b/application/src/pages/api/stats.ts @@ -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): Promise => { - if (!cache.has(CACHE_KEY)) { - console.info('Retrieving new stats') - const data = await prisma.$queryRaw` + 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(` select n."softwareName" as softwarename, count(n.id) as nodecount, ( @@ -40,9 +45,9 @@ const handleGetStats = async (req: NextApiRequest, res: NextApiResponse 1 - order by nodecount desc; - ` - cache.set(CACHE_KEY, { + order by ${query.sortBy.toLowerCase() + ' ' + query.sortWay.toUpperCase()}; + `) + cache.set(cacheKey, { softwares: data.map( (item: StatsItem): StatsResponseSoftware => { return { @@ -56,9 +61,9 @@ const handleGetStats = async (req: NextApiRequest, res: NextApiResponse(CACHE_KEY)) + .json(cache.get(cacheKey)) } export default handleGetStats diff --git a/application/src/pages/index.tsx b/application/src/pages/index.tsx index 20cc8f6..7d7d84b 100644 --- a/application/src/pages/index.tsx +++ b/application/src/pages/index.tsx @@ -131,7 +131,7 @@ const Home:React.FC> = ({ Search - + { loaded ? diff --git a/application/src/pages/stats.tsx b/application/src/pages/stats.tsx index e016891..3aafe3f 100644 --- a/application/src/pages/stats.tsx +++ b/application/src/pages/stats.tsx @@ -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> = ({ matomoConfig }) => { const [loading, setLoading] = useState(true) - const [stats, setStats] = useState(null) + const [loaded, setLoaded] = useState(false) + const [stats, setStats] = useState(null) + const [sort, setSort] = useState({ + 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 ( {'Stats | ' + siteTitle}

Index stats

- - {stats === null - ? (

Failed to load stats data!

) - : ( - <> - - - - - - - - - - - { - stats.softwares.map((software, index) => { - summary.nodeCount += software.nodeCount - summary.accountCount += software.accountCount - summary.channelCount += software.channelCount - return ( - - - - - - - ) - }) - } - - - - - - - - - -
SoftwareInstance countAccount countChannel count
{software.name !== null - ? - : Not recognized}{software.nodeCount}{software.accountCount}{software.channelCount}
Summary{summary.nodeCount}{summary.accountCount}{summary.channelCount}
- - ) - } -
+ + + + + + + + + + + {stats === null + ? () + : ( + <> + + { + stats.softwares.map((software, index) => { + return software.name !== null + ? ( + + + + + + + ) + : ( + + + + + + + ) + }) + } + + + + + + + + + + + ) + } + +
+ + Software name + + + + Instance count + + + + Account count + + + + Channel count + +
Failed to load stats data!
+ + + {software.nodeCount} + + + {software.accountCount} + + + {software.channelCount} + +
Not recognized{software.nodeCount}{software.accountCount} + {software.channelCount} +
Summary{sum.nodeCount}{sum.accountCount}{sum.channelCount}
) } diff --git a/application/src/styles/global.scss b/application/src/styles/global.scss index 66b567c..c3207fc 100644 --- a/application/src/styles/global.scss +++ b/application/src/styles/global.scss @@ -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; diff --git a/application/src/types/StatsRequest.ts b/application/src/types/StatsRequest.ts new file mode 100644 index 0000000..9d92c70 --- /dev/null +++ b/application/src/types/StatsRequest.ts @@ -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 +export type StatsRequestSortWay = z.infer +export type StatsRequestSortBy = z.infer