2025-04-01 16:15:20 +02:00
|
|
|
// Dependencies
|
|
|
|
import { linkClasses, formatNumber, orderBy, clone } from '@freesewing/utils'
|
|
|
|
// Hooks
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
|
|
import { useBackend } from '@freesewing/react/hooks/useBackend'
|
|
|
|
// Components
|
|
|
|
import { Spinner } from '@freesewing/react/components/Spinner'
|
|
|
|
import { Link as WebLink } from '@freesewing/react/components/Link'
|
|
|
|
import { ChartWrapper } from '@freesewing/react/components/Echart'
|
2025-05-30 11:29:55 +02:00
|
|
|
import { Popout } from '@freesewing/react/components/Popout'
|
2025-04-01 16:15:20 +02:00
|
|
|
|
|
|
|
const option = {
|
|
|
|
tooltip: {
|
|
|
|
trigger: 'axis',
|
|
|
|
show: true,
|
|
|
|
axisPointer: {
|
|
|
|
type: 'line',
|
|
|
|
lineStyle: {
|
|
|
|
type: 'dashed',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
title: {
|
|
|
|
left: 'center',
|
|
|
|
},
|
|
|
|
grid: {
|
|
|
|
left: '40',
|
|
|
|
right: '60',
|
|
|
|
containLabel: true,
|
|
|
|
},
|
|
|
|
toolbox: {
|
|
|
|
feature: {
|
|
|
|
saveAsImage: {},
|
|
|
|
magicType: {
|
|
|
|
type: ['line', 'bar'],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
yAxis: {
|
|
|
|
type: 'value',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2025-05-29 12:21:39 +02:00
|
|
|
/**
|
|
|
|
* A component to display generic stats from the FreeSewing backend
|
|
|
|
*
|
|
|
|
* @component
|
|
|
|
* @param {object} props - All component props
|
|
|
|
* @param {React.FC} [props.Link = false] - An optional framework-specific Link component
|
|
|
|
* @returns {JSX.Element}
|
|
|
|
*/
|
2025-04-01 16:15:20 +02:00
|
|
|
export const Stats = ({ Link = false }) => {
|
|
|
|
if (!Link) Link = WebLink
|
|
|
|
const [stats, setStats] = useState()
|
|
|
|
const [error, setError] = useState(false)
|
|
|
|
const backend = useBackend()
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
getStats(backend, setStats, setError)
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
if (!stats)
|
|
|
|
return (
|
2025-04-18 08:07:13 +00:00
|
|
|
<div className="tw:text-center tw:mt-12 tw:w-full">
|
2025-04-01 16:15:20 +02:00
|
|
|
<Spinner />
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
|
|
|
|
const designTop = orderBy(
|
|
|
|
Object.entries(stats.designs).map(([design, count]) => ({ design, count })),
|
|
|
|
'count',
|
|
|
|
'desc'
|
|
|
|
).slice(0, 50)
|
|
|
|
|
|
|
|
const optionD = clone(option)
|
|
|
|
optionD.title.text = 'Top 50 FreeSewing Designs'
|
|
|
|
optionD.xAxis = {
|
|
|
|
type: 'category',
|
|
|
|
data: designTop.map((entry) => entry.design),
|
|
|
|
name: 'Design',
|
|
|
|
}
|
|
|
|
optionD.series = [
|
|
|
|
{
|
|
|
|
data: designTop.map((entry) => entry.count),
|
|
|
|
type: 'bar',
|
|
|
|
},
|
|
|
|
]
|
|
|
|
|
|
|
|
const optionU = clone(option)
|
|
|
|
optionU.title.text = 'Top 25 FreeSewing Users'
|
|
|
|
optionU.xAxis = {
|
|
|
|
type: 'category',
|
|
|
|
data: stats.topUsers.map((user) => user.username),
|
|
|
|
name: 'User',
|
|
|
|
}
|
|
|
|
optionU.series = [
|
|
|
|
{
|
|
|
|
data: stats.topUsers.map((user) => user.calls),
|
|
|
|
type: 'bar',
|
|
|
|
},
|
|
|
|
]
|
|
|
|
|
2025-05-30 11:29:55 +02:00
|
|
|
if (error)
|
|
|
|
return (
|
|
|
|
<Popout type="error" title="Something went wrong" compact>
|
|
|
|
This is unexpected. You may want to report this.
|
|
|
|
</Popout>
|
|
|
|
)
|
|
|
|
|
2025-04-01 16:15:20 +02:00
|
|
|
return (
|
|
|
|
<>
|
2025-04-18 08:07:13 +00:00
|
|
|
<div className="tw:max-w-7xl tw:mx-auto tw:my-12 tw:px-4">
|
|
|
|
<div className="tw:grid tw:grid-cols-1 tw:md:grid-cols-2 tw:gap-2">
|
2025-04-01 16:15:20 +02:00
|
|
|
<Stat title="Users" value={stats.user} />
|
|
|
|
<Stat title="Patterns" value={stats.pattern} />
|
|
|
|
<Stat title="Measurements Sets" value={stats.set} />
|
|
|
|
<Stat title="Curated Sets" value={stats.curatedSet} />
|
|
|
|
<Stat title="Bookmarks" value={stats.bookmark} />
|
|
|
|
<Stat title="API Keys" value={stats.apikey} />
|
|
|
|
<Stat title="JWT Calls" value={stats.activity.jwt} desc="Total Number Seen" />
|
|
|
|
<Stat title="API Key Calls" value={stats.activity.key} desc="Total Number Seen" />
|
|
|
|
</div>
|
|
|
|
<h2>Top Users</h2>
|
|
|
|
<ChartWrapper option={optionU} />
|
2025-04-18 08:07:13 +00:00
|
|
|
<small className="tw:ml-4 tw:py-1 tw:opacity-80">
|
2025-04-01 16:15:20 +02:00
|
|
|
<b>Note:</b> Ordered by JWT calls made to the FreeSewing backend
|
|
|
|
</small>
|
2025-04-18 08:07:13 +00:00
|
|
|
<div className="tw:max-h-96 tw:overflow-scroll">
|
|
|
|
<ol className="tw:list tw:list-inside tw:list-decimal tw:ml-4">
|
2025-04-01 16:15:20 +02:00
|
|
|
{stats.topUsers.map((u) => (
|
|
|
|
<li key={u.id}>
|
2025-04-12 10:42:35 +00:00
|
|
|
<Link href={`/users/?id=${u.id}`} className={linkClasses}>
|
2025-04-01 16:15:20 +02:00
|
|
|
{u.username}
|
|
|
|
</Link>
|
|
|
|
: {formatNumber(u.calls)}
|
|
|
|
</li>
|
|
|
|
))}
|
|
|
|
</ol>
|
|
|
|
</div>
|
|
|
|
<h2>Top Designs</h2>
|
|
|
|
<ChartWrapper option={optionD} />
|
2025-04-18 08:07:13 +00:00
|
|
|
<small className="tw:ml-4 tw:py-1 tw:opacity-80">
|
2025-04-01 16:15:20 +02:00
|
|
|
<b>Note:</b> Ordered by patterns stored in the FreeSewing backend
|
|
|
|
</small>
|
2025-04-18 08:07:13 +00:00
|
|
|
<div className="tw:max-h-96 tw:overflow-scroll">
|
|
|
|
<ol className="tw:list tw:list-inside tw:list-decimal tw:ml-4">
|
2025-04-01 16:15:20 +02:00
|
|
|
{Object.entries(stats.designs).map(([d, c]) => (
|
|
|
|
<li key={d}>
|
|
|
|
<Link href={`/designs/${d}`} className={linkClasses}>
|
2025-04-18 08:07:13 +00:00
|
|
|
<span className="tw:capitalize">{d}</span>
|
2025-04-01 16:15:20 +02:00
|
|
|
</Link>
|
|
|
|
: {formatNumber(c)}
|
|
|
|
</li>
|
|
|
|
))}
|
|
|
|
</ol>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const Stat = ({ title, value, desc = 'Total Number Stored' }) => (
|
2025-04-18 08:07:13 +00:00
|
|
|
<div className="tw:daisy-stats tw:shadow">
|
|
|
|
<div className="tw:daisy-stat">
|
|
|
|
<div className="tw:daisy-stat-title">{title}</div>
|
|
|
|
<div className="tw:daisy-stat-value">{formatNumber(value)}</div>
|
|
|
|
<div className="tw:daisy-stat-desc">{desc}</div>
|
2025-04-01 16:15:20 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
|
|
|
|
const getStats = async (backend, setStats, setError) => {
|
|
|
|
const result = await backend.getStats()
|
|
|
|
if (result[0] === 200 && result[1]) setStats(result[1])
|
|
|
|
else setError(true)
|
|
|
|
}
|