Improve stats page (#1530)

Signed-off-by: Cintia Sanchez Garcia <cynthiasg@icloud.com>
This commit is contained in:
Cynthia Sánchez García 2021-09-02 16:48:54 +02:00 committed by GitHub
parent b95e45313d
commit a39d407bd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 585 additions and 146 deletions

View File

@ -6,7 +6,7 @@
"@analytics/google-analytics": "^0.3.1",
"@apidevtools/json-schema-ref-parser": "^9.0.6",
"analytics": "^0.3.5",
"apexcharts": "^3.26.0",
"apexcharts": "^3.27.3",
"bootstrap": "^4.5.2",
"classnames": "^2.2.6",
"codemirror": "^5.57.0",
@ -16,7 +16,7 @@
"lodash": "^4.17.21",
"moment": "^2.27.0",
"react": "^16.13.1",
"react-apexcharts": "^1.3.7",
"react-apexcharts": "^1.3.9",
"react-codemirror2": "^7.2.1",
"react-color": "^2.19.3",
"react-dom": "^16.13.1",

View File

@ -0,0 +1,249 @@
import moment from 'moment';
import React, { useEffect } from 'react';
import ReactApexChart from 'react-apexcharts';
import useBreakpointDetect from '../../hooks/useBreakpointDetect';
import getMetaTag from '../../utils/getMetaTag';
import prettifyNumber from '../../utils/prettifyNumber';
interface Props {
series: any[];
id: string;
title: string;
activeTheme: string;
}
const BrushChart = (props: Props) => {
const primaryColor = getMetaTag('primaryColor');
const secondaryColor = getMetaTag('secondaryColor');
const point = useBreakpointDetect();
useEffect(() => {
// We force to use original selection after changing breakpoint
ApexCharts.exec(
`${props.id}BrushChart`,
'updateOptions',
{
selection: {
xaxis: {
min: moment().subtract(12, 'months').valueOf(), // We select last year
max: moment.now(),
},
},
},
false,
true
);
}, [point, props.id]);
const getBarChartConfig = (): ApexCharts.ApexOptions => {
return {
chart: {
id: `${props.id}BarChart`,
height: 300,
type: 'bar',
zoom: {
enabled: false,
},
redrawOnWindowResize: false,
fontFamily: "'Lato', Roboto, 'Helvetica Neue', Arial, sans-serif !default",
toolbar: {
show: false,
},
},
grid: { borderColor: 'var(--border-md)' },
plotOptions: {
bar: {
borderRadius: 5,
dataLabels: {
position: 'top',
},
},
},
dataLabels: {
enabled: true,
offsetY: -20,
style: {
fontSize: '12px',
colors: ['var(--color-font)'],
},
formatter: (value: number) => {
return prettifyNumber(value);
},
},
colors: ['var(--color-1-500)'],
xaxis: {
type: 'datetime',
position: 'bottom',
axisBorder: {
show: false,
},
axisTicks: {
show: false,
},
tooltip: {
enabled: true,
},
labels: {
format: `MM/yy`,
style: {
colors: 'var(--color-font)',
},
},
},
yaxis: {
labels: {
style: {
colors: 'var(--color-font)',
},
},
},
tooltip: {
x: {
format: `MMM'yy`,
},
},
title: {
text: props.title,
style: {
color: 'var(--color-font)',
},
},
responsive: [
{
breakpoint: 1920,
options: {
plotOptions: {
bar: {
columnWidth: '80%',
},
},
dataLabels: {
offsetY: -15,
style: {
fontSize: '9px',
},
},
},
},
{
breakpoint: 768,
options: {
dataLabels: {
enabled: false,
},
},
},
],
};
};
const getAreaChartConfig = (): ApexCharts.ApexOptions => {
return {
chart: {
id: `${props.id}BrushChart`,
height: 120,
type: 'area',
zoom: {
enabled: false,
},
redrawOnWindowResize: false,
brush: {
target: `${props.id}BarChart`,
enabled: true,
autoScaleYaxis: false,
},
selection: {
enabled: true,
xaxis: {
min: moment().subtract(12, 'months').valueOf(), // We select last year
max: moment.now(),
},
fill: {
color: props.activeTheme === 'dark' ? '#222529' : secondaryColor,
opacity: 0.2,
},
stroke: {
width: 1,
dashArray: 4,
color: props.activeTheme === 'dark' ? '#222529' : secondaryColor,
opacity: 1,
},
},
fontFamily: "'Lato', Roboto, 'Helvetica Neue', Arial, sans-serif !default",
toolbar: {
show: false,
},
},
grid: { borderColor: 'var(--border-md)' },
plotOptions: {
bar: {
borderRadius: 5,
dataLabels: {
position: 'top',
},
},
},
colors: ['var(--color-1-500)'],
xaxis: {
type: 'datetime',
position: 'bottom',
axisBorder: {
show: false,
},
axisTicks: {
show: false,
},
tooltip: {
enabled: true,
},
labels: {
format: `MM/yy`,
style: {
colors: 'var(--color-font)',
},
},
},
yaxis: {
labels: {
style: {
colors: 'var(--color-font)',
},
},
},
stroke: {
curve: 'smooth',
},
fill: {
opacity: 0.5,
colors: [
() => {
return props.activeTheme === 'dark' ? '#222529' : primaryColor;
},
],
},
};
};
return (
<>
<ReactApexChart
options={getBarChartConfig()}
series={[
{
name: 'Packages',
data: props.series,
},
]}
type="bar"
height={300}
/>
{/* Brush chart is only visible when we have collected data from more than one year */}
{props.series.length > 12 && (
<ReactApexChart options={getAreaChartConfig()} series={[{ data: props.series }]} type="area" height={130} />
)}
</>
);
};
export default BrushChart;

View File

@ -5,24 +5,6 @@ exports[`StatsView creates snapshot 1`] = `
<div
class="d-flex flex-column flex-grow-1 position-relative"
>
<div
class="position-absolute p-5 wrapper undefined"
>
<div
class="d-flex flex-row align-items-center justify-content-center w-100 h-100"
>
<div
class="spinner-border text-primary spinner undefined"
role="status"
>
<span
class="sr-only"
>
Loading...
</span>
</div>
</div>
</div>
<main
class="container-lg px-sm-4 px-lg-0 py-5 noFocus"
id="content"
@ -37,6 +19,295 @@ exports[`StatsView creates snapshot 1`] = `
>
Stats
</div>
<div
class="text-center mb-5"
>
<small>
<span
class="text-muted mr-2"
>
Report generated at:
</span>
</small>
</div>
<span
class="header"
>
<h2
class="position-relative anchorHeader headingWrapper mb-4 font-weight-bold title"
>
<div
class="position-absolute headerAnchor"
data-testid="anchor"
id="packages-and-releases"
/>
<a
aria-label="Packages and releases"
class="text-reset text-center d-none d-md-block headingLink"
href="/#packages-and-releases"
role="button"
>
<svg
fill="currentColor"
height="1em"
stroke="currentColor"
stroke-width="0"
viewBox="0 0 16 16"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"
fill-rule="evenodd"
/>
</svg>
</a>
Packages and releases
</h2>
</span>
<div
class="row my-4 pb-0 pb-lg-4"
>
<div
class="col-12 col-lg-6"
>
<div
class="pr-0 pr-lg-3 pr-xxl-4 mt-4 mb-4 mb-lg-0"
>
<div
class="card chartWrapper"
>
<div
class="position-absolute p-5 wrapper undefined"
>
<div
class="d-flex flex-row align-items-center justify-content-center w-100 h-100"
>
<div
class="spinner-border text-primary spinner undefined"
role="status"
>
<span
class="sr-only"
>
Loading...
</span>
</div>
</div>
</div>
<div>
Chart
</div>
</div>
</div>
</div>
<div
class="col-12 col-lg-6"
>
<div
class="pl-0 pl-lg-3 pl-xxl-4 mt-4"
>
<div
class="card chartWrapper"
>
<div
class="position-absolute p-5 wrapper undefined"
>
<div
class="d-flex flex-row align-items-center justify-content-center w-100 h-100"
>
<div
class="spinner-border text-primary spinner undefined"
role="status"
>
<span
class="sr-only"
>
Loading...
</span>
</div>
</div>
</div>
<div>
Chart
</div>
</div>
</div>
</div>
</div>
<span
class="header"
>
<h2
class="position-relative anchorHeader headingWrapper my-4 font-weight-bold title"
>
<div
class="position-absolute headerAnchor"
data-testid="anchor"
id="repositories"
/>
<a
aria-label="Repositories"
class="text-reset text-center d-none d-md-block headingLink"
href="/#repositories"
role="button"
>
<svg
fill="currentColor"
height="1em"
stroke="currentColor"
stroke-width="0"
viewBox="0 0 16 16"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"
fill-rule="evenodd"
/>
</svg>
</a>
Repositories
</h2>
</span>
<div
class="row my-4"
>
<div
class="col-12 my-4"
>
<div
class="card chartWrapper"
>
<div
class="position-absolute p-5 wrapper undefined"
>
<div
class="d-flex flex-row align-items-center justify-content-center w-100 h-100"
>
<div
class="spinner-border text-primary spinner undefined"
role="status"
>
<span
class="sr-only"
>
Loading...
</span>
</div>
</div>
</div>
<div>
Chart
</div>
</div>
</div>
</div>
<span
class="header"
>
<h2
class="position-relative anchorHeader headingWrapper mt-4 font-weight-bold title"
>
<div
class="position-absolute headerAnchor"
data-testid="anchor"
id="organizations-and-users"
/>
<a
aria-label="Organizations and users"
class="text-reset text-center d-none d-md-block headingLink"
href="/#organizations-and-users"
role="button"
>
<svg
fill="currentColor"
height="1em"
stroke="currentColor"
stroke-width="0"
viewBox="0 0 16 16"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"
fill-rule="evenodd"
/>
</svg>
</a>
Organizations and users
</h2>
</span>
<div
class="row my-4"
>
<div
class="col-12 col-lg-6"
>
<div
class="pr-0 pr-lg-3 pr-xxl-4 pt-4"
>
<div
class="card chartWrapper"
>
<div
class="position-absolute p-5 wrapper undefined"
>
<div
class="d-flex flex-row align-items-center justify-content-center w-100 h-100"
>
<div
class="spinner-border text-primary spinner undefined"
role="status"
>
<span
class="sr-only"
>
Loading...
</span>
</div>
</div>
</div>
<div>
Chart
</div>
</div>
</div>
</div>
<div
class="col-12 col-lg-6"
>
<div
class="pl-0 pl-lg-3 pl-xxl-4 pt-4"
>
<div
class="card chartWrapper"
>
<div
class="position-absolute p-5 wrapper undefined"
>
<div
class="d-flex flex-row align-items-center justify-content-center w-100 h-100"
>
<div
class="spinner-border text-primary spinner undefined"
role="status"
>
<span
class="sr-only"
>
Loading...
</span>
</div>
</div>
</div>
<div>
Chart
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>

View File

@ -8,6 +8,7 @@ import { AppCtx } from '../../context/AppCtx';
import { AHStats, ErrorKind } from '../../types';
import StatsView from './index';
jest.mock('../../api');
jest.mock('./BrushChart', () => () => <div>Chart</div>);
jest.mock('react-apexcharts', () => () => <div>Chart</div>);
const getMockStats = (fixtureId: string): AHStats => {

View File

@ -11,10 +11,10 @@ import { AHStats } from '../../types';
import compoundErrorMessage from '../../utils/compoundErrorMessage';
import getMetaTag from '../../utils/getMetaTag';
import isWhiteLabel from '../../utils/isWhiteLabel';
import prettifyNumber from '../../utils/prettifyNumber';
import AnchorHeader from '../common/AnchorHeader';
import Loading from '../common/Loading';
import NoData from '../common/NoData';
import BrushChart from './BrushChart';
import styles from './StatsView.module.css';
interface Props {
@ -30,7 +30,28 @@ const StatsView = (props: Props) => {
const { effective } = ctx.prefs.theme;
const [activeTheme, setActiveTheme] = useState(effective);
const [isLoading, setIsLoading] = useState(false);
const [stats, setStats] = useState<AHStats | null | undefined>(undefined);
const [stats, setStats] = useState<AHStats | null>({
packages: {
total: 0,
runningTotal: [],
},
snapshots: {
total: 0,
runningTotal: [],
},
repositories: {
total: 0,
runningTotal: [],
},
organizations: {
total: 0,
runningTotal: [],
},
users: {
total: 0,
runningTotal: [],
},
});
const [apiError, setApiError] = useState<string | null>(null);
useEffect(() => {
@ -142,7 +163,6 @@ const StatsView = (props: Props) => {
},
xaxis: {
type: 'datetime',
tickPlacement: 'on',
labels: {
datetimeFormatter: {
year: 'yyyy',
@ -169,103 +189,6 @@ const StatsView = (props: Props) => {
};
};
const getBarChartConfig = (title: string): ApexCharts.ApexOptions => {
return {
chart: {
height: 300,
type: 'bar',
fontFamily: "'Lato', Roboto, 'Helvetica Neue', Arial, sans-serif !default",
toolbar: {
show: false,
},
},
grid: { borderColor: 'var(--border-md)' },
plotOptions: {
bar: {
borderRadius: 5,
dataLabels: {
position: 'top',
},
},
},
dataLabels: {
enabled: true,
offsetY: -20,
style: {
fontSize: '12px',
colors: ['var(--color-font)'],
},
formatter: (value: number) => {
return prettifyNumber(value);
},
},
colors: ['var(--color-1-500)'],
xaxis: {
type: 'datetime',
position: 'bottom',
axisBorder: {
show: false,
},
axisTicks: {
show: false,
},
tooltip: {
enabled: true,
},
labels: {
format: `MM/yy`,
style: {
colors: 'var(--color-font)',
},
},
},
yaxis: {
labels: {
style: {
colors: 'var(--color-font)',
},
},
},
tooltip: {
x: {
format: `MMM'yy`,
},
},
title: {
text: title,
style: {
color: 'var(--color-font)',
},
},
responsive: [
{
breakpoint: 1920,
options: {
plotOptions: {
bar: {
columnWidth: '80%',
},
},
dataLabels: {
offsetY: -15,
style: {
fontSize: '9px',
},
},
},
},
{
breakpoint: 768,
options: {
dataLabels: {
enabled: false,
},
},
},
],
};
};
useEffect(() => {
async function getStats() {
try {
@ -309,8 +232,6 @@ const StatsView = (props: Props) => {
return (
<div className="d-flex flex-column flex-grow-1 position-relative">
{(isUndefined(stats) || isLoading) && <Loading />}
<main role="main" className="container-lg px-sm-4 px-lg-0 py-5 noFocus" id="content" tabIndex={-1}>
<div className="flex-grow-1 position-relative">
<div className={`h2 text-dark text-center ${styles.title}`}>{siteName} Stats</div>
@ -322,7 +243,7 @@ const StatsView = (props: Props) => {
<div className="text-center mb-5">
<small>
<span className="text-muted mr-2">Report generated at:</span>
{moment(stats.generatedAt).format('YYYY/MM/DD HH:mm:ss (Z)')}
{stats.generatedAt ? moment(stats.generatedAt).format('YYYY/MM/DD HH:mm:ss (Z)') : ''}
</small>
</div>
@ -344,6 +265,7 @@ const StatsView = (props: Props) => {
<div className={classnames('col-12', { 'col-lg-6': stats.snapshots.runningTotal })}>
<div className="pr-0 pr-lg-3 pr-xxl-4 mt-4 mb-4 mb-lg-0">
<div className={`card ${styles.chartWrapper}`}>
{(stats.snapshots.runningTotal!.length === 0 || isLoading) && <Loading />}
<ReactApexChart
options={getAreaChartConfig('Packages available')}
series={[{ name: 'Packages', data: stats.packages.runningTotal }]}
@ -359,6 +281,7 @@ const StatsView = (props: Props) => {
<div className={classnames('col-12', { 'col-lg-6': stats.packages.runningTotal })}>
<div className="pl-0 pl-lg-3 pl-xxl-4 mt-4">
<div className={`card ${styles.chartWrapper}`}>
{(stats.packages.runningTotal!.length === 0 || isLoading) && <Loading />}
<ReactApexChart
options={getAreaChartConfig('Releases available')}
series={[{ name: 'Releases', data: stats.snapshots.runningTotal }]}
@ -378,16 +301,12 @@ const StatsView = (props: Props) => {
<div className={classnames('col-12', { 'col-lg-6': stats.snapshots.createdMonthly })}>
<div className="pr-0 pr-lg-3 pr-xxl-4 mt-4 mb-4 mb-lg-0">
<div className={`card ${styles.chartWrapper}`}>
<ReactApexChart
options={getBarChartConfig('New packages added monthly')}
series={[
{
name: 'Packages',
data: stats.packages.createdMonthly,
},
]}
type="bar"
height={300}
{(stats.packages.createdMonthly!.length === 0 || isLoading) && <Loading />}
<BrushChart
series={stats.packages.createdMonthly}
title="New packages added monthly"
id="snapshots"
activeTheme={activeTheme}
/>
</div>
</div>
@ -398,16 +317,12 @@ const StatsView = (props: Props) => {
<div className={classnames('col-12', { 'col-lg-6': stats.packages.createdMonthly })}>
<div className="pl-0 pl-lg-3 pl-xxl-4 mt-4">
<div className={`card ${styles.chartWrapper}`}>
<ReactApexChart
options={getBarChartConfig('New releases added monthly')}
series={[
{
name: 'Releases',
data: stats.snapshots.createdMonthly,
},
]}
type="bar"
height={300}
{(stats.snapshots.createdMonthly!.length === 0 || isLoading) && <Loading />}
<BrushChart
series={stats.snapshots.createdMonthly}
title="New packages added monthly"
id="packages"
activeTheme={activeTheme}
/>
</div>
</div>
@ -430,6 +345,7 @@ const StatsView = (props: Props) => {
<div className="row my-4">
<div className="col-12 my-4">
<div className={`card ${styles.chartWrapper}`}>
{(stats.repositories.runningTotal!.length === 0 || isLoading) && <Loading />}
<ReactApexChart
options={getAreaChartConfig('Registered repositories')}
series={[{ name: 'Repositories', data: stats.repositories.runningTotal }]}
@ -455,6 +371,7 @@ const StatsView = (props: Props) => {
<div className={classnames('col-12', { 'col-lg-6': stats.users.runningTotal })}>
<div className="pr-0 pr-lg-3 pr-xxl-4 pt-4">
<div className={`card ${styles.chartWrapper}`}>
{(stats.organizations.runningTotal!.length === 0 || isLoading) && <Loading />}
<ReactApexChart
options={getAreaChartConfig('Registered organizations', true)}
series={[
@ -475,6 +392,7 @@ const StatsView = (props: Props) => {
<div className={classnames('col-12', { 'col-lg-6': stats.organizations.runningTotal })}>
<div className="pl-0 pl-lg-3 pl-xxl-4 pt-4">
<div className={`card ${styles.chartWrapper}`}>
{(stats.users.runningTotal!.length === 0 || isLoading) && <Loading />}
<ReactApexChart
options={getAreaChartConfig('Registered users', true)}
series={[

View File

@ -646,7 +646,7 @@ export enum ChartTemplateSpecialType {
}
export interface AHStats {
generatedAt: number;
generatedAt?: number;
packages: {
total: number;
runningTotal?: any[];

View File

@ -2421,7 +2421,7 @@ anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
apexcharts@^3.26.0:
apexcharts@^3.27.3:
version "3.28.1"
resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.28.1.tgz#35a17ea9ef9a1a93fb01ce79d245af8aedb59a7b"
integrity sha512-5M1KitI/XmY2Sx6ih9vQOXyQUTmotDG/cML2N6bkBlVseF10RPSzM7dkrf7Y68apSZF6e7J581gXXu1+qkLhCA==
@ -10215,7 +10215,7 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
react-apexcharts@^1.3.7:
react-apexcharts@^1.3.9:
version "1.3.9"
resolved "https://registry.yarnpkg.com/react-apexcharts/-/react-apexcharts-1.3.9.tgz#d97e53fd513dc6ff73b90c2364c3bdd88d8dad01"
integrity sha512-KPonT5uQPHOHSVgTNEzpB0HhCkZtoicQYGjR9P+3DRDSgTsC+DM2vDUfo/B2Fn1m+wdgVeDXWL0VJYDc6JD/tw==