mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-02-10 09:55:11 +08:00
[refactor] Fix admin panel statistics rendering in some info.json responses
This commit is contained in:
@ -0,0 +1 @@
|
||||
npx lint-staged
|
||||
|
||||
@ -1,41 +1,64 @@
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function InfoTable({caption, editor, viewer, desc}) {
|
||||
/**
|
||||
* Renders a two-section info table for Editor and Live Viewer values.
|
||||
* Values can optionally include a status class in v[1] like 'critical' or 'normal'.
|
||||
* Sections can be toggled via the `mode` prop: 'all' | 'edit' | 'view'.
|
||||
*
|
||||
* @param {{
|
||||
* caption?: string,
|
||||
* editor: Array<[number|string, ("critical"|"normal")?]>,
|
||||
* viewer: Array<[number|string, ("critical"|"normal")?]>,
|
||||
* desc: string[],
|
||||
* mode?: 'all' | 'edit' | 'view'
|
||||
* }} props
|
||||
*/
|
||||
export default function InfoTable({caption, editor, viewer, desc, mode = 'all'}) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{caption && <div className={styles.sectionHeader}>{caption}</div>}
|
||||
<div className={styles.editorsLabel}>EDITORS</div>
|
||||
<div className={styles.divider}></div>
|
||||
<div className={styles.row}>
|
||||
{editor.map((v, i) => (
|
||||
<div key={i} className={`${styles.valueCell} ${desc[i] === 'Remaining' ? styles.remainingValue : ''}`}>
|
||||
{v[0]}
|
||||
|
||||
{mode !== 'view' && (
|
||||
<>
|
||||
<div className={styles.editorsLabel}>EDITORS</div>
|
||||
<div className={styles.divider}></div>
|
||||
<div className={styles.row}>
|
||||
{[0, 1, 2, 3].map(i => (
|
||||
<div key={i} className={`${styles.valueCell} ${editor[i] && editor[i][1] ? styles[editor[i][1]] : ''}`}>
|
||||
{editor[i] && editor[i][0] !== undefined ? editor[i][0] : ''}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
{desc.map((d, i) => (
|
||||
<div key={i} className={styles.labelCell}>
|
||||
{d}
|
||||
<div className={styles.row}>
|
||||
{[0, 1, 2, 3].map(i => (
|
||||
<div key={i} className={styles.labelCell}>
|
||||
{desc[i] || ''}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.viewerLabel}>LIVE VIEWER</div>
|
||||
<div className={styles.divider}></div>
|
||||
<div className={styles.row}>
|
||||
{viewer.map((v, i) => (
|
||||
<div key={i} className={`${styles.valueCell} ${desc[i] === 'Remaining' ? styles.remainingValue : ''}`}>
|
||||
{v[0]}
|
||||
</>
|
||||
)}
|
||||
|
||||
{mode !== 'edit' && (
|
||||
<>
|
||||
<div className={styles.viewerLabel}>LIVE VIEWER</div>
|
||||
<div className={styles.divider}></div>
|
||||
<div className={styles.row}>
|
||||
{[0, 1, 2, 3].map(i => (
|
||||
<div key={i} className={`${styles.valueCell} ${viewer[i] && viewer[i][1] ? styles[viewer[i][1]] : ''}`}>
|
||||
{viewer[i] && viewer[i][0] !== undefined ? viewer[i][0] : ''}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
{desc.map((d, i) => (
|
||||
<div key={i} className={styles.labelCell}>
|
||||
{d}
|
||||
<div className={styles.row}>
|
||||
{[0, 1, 2, 3].map(i => (
|
||||
<div key={i} className={styles.labelCell}>
|
||||
{desc[i] || ''}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,23 +2,26 @@
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.sectionHeader {
|
||||
background: #f5f5f5;
|
||||
background: #efefef;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
.valueCell {
|
||||
font-size: 22px;
|
||||
font-size: 26px;
|
||||
line-height: 28px;
|
||||
padding: 8px 0;
|
||||
width: 25%;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.labelCell {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
padding-bottom: 8px;
|
||||
width: 25%;
|
||||
@ -36,10 +39,18 @@
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-bottom: 1px solid #dadada;
|
||||
margin: 2px 10px;
|
||||
}
|
||||
|
||||
.remainingValue {
|
||||
color: rgb(1, 125, 28);
|
||||
}
|
||||
|
||||
/* match branding coloring */
|
||||
.critical {
|
||||
color: #ff0000;
|
||||
}
|
||||
.normal {
|
||||
color: #017d1c;
|
||||
}
|
||||
|
||||
28
AdminPanel/client/src/components/Statistics/ModeSwitcher.js
Normal file
28
AdminPanel/client/src/components/Statistics/ModeSwitcher.js
Normal file
@ -0,0 +1,28 @@
|
||||
import styles from './styles.module.css';
|
||||
|
||||
/**
|
||||
* Mode switcher component for statistics view.
|
||||
* Persists selected mode to localStorage via parent.
|
||||
*
|
||||
* @param {{
|
||||
* mode: 'all'|'edit'|'view',
|
||||
* setMode: (mode: 'all'|'edit'|'view') => void
|
||||
* }} props
|
||||
*/
|
||||
export default function ModeSwitcher({mode, setMode}) {
|
||||
return (
|
||||
<div className={styles.modeBar}>
|
||||
<span className={`${styles.modeLink} ${mode === 'all' ? styles.current : ''}`} onClick={() => setMode('all')}>
|
||||
All
|
||||
</span>
|
||||
<span className={styles.modeSeparator}>|</span>
|
||||
<span className={`${styles.modeLink} ${mode === 'edit' ? styles.current : ''}`} onClick={() => setMode('edit')}>
|
||||
Editors
|
||||
</span>
|
||||
<span className={styles.modeSeparator}>|</span>
|
||||
<span className={`${styles.modeLink} ${mode === 'view' ? styles.current : ''}`} onClick={() => setMode('view')}>
|
||||
Live Viewer
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
import {memo, useMemo} from 'react';
|
||||
import InfoTable from './InfoTable/index';
|
||||
|
||||
const MILLISECONDS_PER_DAY = 86400000;
|
||||
|
||||
/**
|
||||
* Count internal/external users.
|
||||
* @param {Record<string, { anonym?: boolean }>} users
|
||||
* @returns {{internal: number, external: number}}
|
||||
*/
|
||||
function countUsers(users = {}) {
|
||||
let internal = 0;
|
||||
let external = 0;
|
||||
for (const uid in users) {
|
||||
if (Object.prototype.hasOwnProperty.call(users, uid)) {
|
||||
users[uid]?.anonym ? external++ : internal++;
|
||||
}
|
||||
}
|
||||
return {internal, external};
|
||||
}
|
||||
|
||||
/**
|
||||
* MonthlyStatistics - renders usage statistics by month.
|
||||
* Mirrors logic from branding/info/index.html fillStatistic().
|
||||
*
|
||||
* @param {{ byMonth?: Array<any>, mode: 'all'|'edit'|'view' }} props
|
||||
*/
|
||||
function MonthlyStatistics({byMonth, mode}) {
|
||||
const periods = useMemo(() => {
|
||||
if (!Array.isArray(byMonth) || byMonth.length < 1) return [];
|
||||
|
||||
// Build periods in chronological order, then reverse for display.
|
||||
const mapped = byMonth
|
||||
.map((item, index) => {
|
||||
const date = item?.date ? new Date(item.date) : null;
|
||||
if (!date) return null;
|
||||
|
||||
const editCounts = countUsers(item?.users);
|
||||
const viewCounts = countUsers(item?.usersView);
|
||||
|
||||
const nextDate = index + 1 < byMonth.length ? new Date(byMonth[index + 1].date) : null;
|
||||
|
||||
return {
|
||||
startDate: date,
|
||||
endDate: nextDate ? new Date(nextDate.getTime() - MILLISECONDS_PER_DAY) : null,
|
||||
internalEdit: editCounts.internal,
|
||||
externalEdit: editCounts.external,
|
||||
internalView: viewCounts.internal,
|
||||
externalView: viewCounts.external
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.reverse();
|
||||
|
||||
return mapped;
|
||||
}, [byMonth]);
|
||||
|
||||
if (periods.length < 1) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{textAlign: 'center', fontWeight: 600, margin: '16px 0'}}>Usage statistics for the reporting period</div>
|
||||
{periods.map((p, idx) => {
|
||||
const caption = p.endDate
|
||||
? `${p.startDate.toLocaleDateString()} - ${p.endDate.toLocaleDateString()}`
|
||||
: `From ${p.startDate.toLocaleDateString()}`;
|
||||
|
||||
const editor = [
|
||||
[p.internalEdit, ''],
|
||||
[p.externalEdit, ''],
|
||||
[p.internalEdit + p.externalEdit, '']
|
||||
];
|
||||
const viewer = [
|
||||
[p.internalView, ''],
|
||||
[p.externalView, ''],
|
||||
[p.internalView + p.externalView, '']
|
||||
];
|
||||
const desc = ['Internal', 'External', 'Active', ''];
|
||||
|
||||
return <InfoTable key={idx} mode={mode} caption={caption} editor={editor} viewer={viewer} desc={desc} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MonthlyStatistics);
|
||||
@ -1,75 +1,231 @@
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {useQuery} from '@tanstack/react-query';
|
||||
import TopBlock from './TopBlock/index';
|
||||
import InfoTable from './InfoTable/index';
|
||||
import ModeSwitcher from './ModeSwitcher';
|
||||
import MonthlyStatistics from './MonthlyStatistics';
|
||||
import styles from './styles.module.css';
|
||||
import {fetchStatistics} from '../../api';
|
||||
|
||||
// Constants
|
||||
const CRITICAL_COLOR = '#ff0000';
|
||||
const CRITICAL_THRESHOLD = 0.1;
|
||||
const TIME_PERIODS = ['hour', 'day', 'week', 'month'];
|
||||
const TIME_PERIOD_LABELS = ['Last Hour', '24 Hours', 'Week', 'Month'];
|
||||
const SECONDS_PER_DAY = 86400;
|
||||
|
||||
/**
|
||||
* Calculate critical status for remaining values
|
||||
* @param {number} remaining - Remaining count
|
||||
* @param {number} limit - Total limit
|
||||
* @returns {string} 'normal' | 'critical'
|
||||
*/
|
||||
const getCriticalStatus = (remaining, limit) => (remaining > limit * CRITICAL_THRESHOLD ? 'normal' : 'critical');
|
||||
|
||||
// ModeSwitcher moved to ./ModeSwitcher (kept behavior, simplified markup/styles)
|
||||
|
||||
/**
|
||||
* Statistics component - renders Document Server statistics
|
||||
* Mirrors branding/info/index.html rendering logic with mode toggling
|
||||
*/
|
||||
export default function Statistics() {
|
||||
const {data, isLoading, error} = useQuery({
|
||||
queryKey: ['statistics'],
|
||||
queryFn: fetchStatistics
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Loading statistics...</div>;
|
||||
if (error) return <div style={{color: 'red'}}>Error: {error.message}</div>;
|
||||
if (!data) return null;
|
||||
const [mode, setMode] = useState(() => {
|
||||
try {
|
||||
const saved = window.localStorage?.getItem('server-info-display-mode');
|
||||
return saved || 'all';
|
||||
} catch {
|
||||
return 'all';
|
||||
}
|
||||
});
|
||||
|
||||
const {licenseInfo, quota, connectionsStat, serverInfo} = data;
|
||||
useEffect(() => {
|
||||
try {
|
||||
window.localStorage?.setItem('server-info-display-mode', mode);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
// Safe defaults to maintain hook order consistency (memoized to avoid dependency changes)
|
||||
const licenseInfo = useMemo(() => data?.licenseInfo ?? {}, [data?.licenseInfo]);
|
||||
const quota = useMemo(() => data?.quota ?? {}, [data?.quota]);
|
||||
const connectionsStat = useMemo(() => data?.connectionsStat ?? {}, [data?.connectionsStat]);
|
||||
const serverInfo = useMemo(() => data?.serverInfo ?? {}, [data?.serverInfo]);
|
||||
|
||||
// Derived values used across multiple components
|
||||
const isUsersModel = licenseInfo.usersCount > 0;
|
||||
const limitEdit = isUsersModel ? licenseInfo.usersCount : licenseInfo.connections;
|
||||
const limitView = isUsersModel ? licenseInfo.usersViewCount : licenseInfo.connectionsView;
|
||||
|
||||
// Build block
|
||||
const buildDate = licenseInfo.buildDate ? new Date(licenseInfo.buildDate).toLocaleDateString() : '';
|
||||
const packageTypeLabel = licenseInfo.packageType === 0 ? 'Open source' : licenseInfo.packageType === 1 ? 'Enterprise Edition' : 'Developer Edition';
|
||||
const buildBlock = (
|
||||
<TopBlock title='Build'>
|
||||
<div>Type: {licenseInfo.packageType === 0 ? 'Open source' : licenseInfo.packageType === 1 ? 'Enterprise Edition' : 'Developer Edition'}</div>
|
||||
<div>Type: {packageTypeLabel}</div>
|
||||
<div>
|
||||
Version: {serverInfo.buildVersion}.{serverInfo.buildNumber}
|
||||
</div>
|
||||
<div>Release date: {buildDate}</div>
|
||||
</TopBlock>
|
||||
);
|
||||
const licenseBlock = (
|
||||
<TopBlock title='License'>
|
||||
{licenseInfo.startDate === null ? (
|
||||
'No license'
|
||||
) : (
|
||||
<div>Start date: {licenseInfo.startDate ? new Date(licenseInfo.startDate).toLocaleDateString() : ''}</div>
|
||||
)}
|
||||
</TopBlock>
|
||||
);
|
||||
const connectionsBlock = (
|
||||
<TopBlock title='Connections limit'>
|
||||
<div>Editors: {licenseInfo.connections}</div>
|
||||
<div>Live Viewer: {licenseInfo.connectionsView}</div>
|
||||
|
||||
// License block (mirrors fillInfo license validity rendering)
|
||||
const licenseBlock = (() => {
|
||||
if (licenseInfo.endDate === null) {
|
||||
return (
|
||||
<TopBlock title='License'>
|
||||
<div>No license</div>
|
||||
</TopBlock>
|
||||
);
|
||||
}
|
||||
const isLimited = licenseInfo.mode & 1 || licenseInfo.mode & 4;
|
||||
const licEnd = new Date(licenseInfo.endDate);
|
||||
const srvDate = new Date(serverInfo.date);
|
||||
const licType = licenseInfo.type;
|
||||
const isInvalid = licType === 2 || licType === 1 || licType === 6 || licType === 11;
|
||||
const isUpdateUnavailable = !isLimited && srvDate > licEnd;
|
||||
const licValidText = isLimited ? 'Valid: ' : 'Updates available: ';
|
||||
const licValidColor = isInvalid || isUpdateUnavailable ? CRITICAL_COLOR : undefined;
|
||||
|
||||
const startDateStr = licenseInfo.startDate ? new Date(licenseInfo.startDate).toLocaleDateString() : '';
|
||||
const isStartCritical = licType === 16 || (licenseInfo.startDate ? new Date(licenseInfo.startDate) > srvDate : false);
|
||||
const trialText = licenseInfo.mode & 1 ? 'Trial' : '';
|
||||
|
||||
return (
|
||||
<TopBlock title='License'>
|
||||
{startDateStr && <div style={isStartCritical ? {color: CRITICAL_COLOR} : undefined}>Start date: {startDateStr}</div>}
|
||||
<div>
|
||||
<span>{licValidText}</span>
|
||||
<span style={licValidColor ? {color: licValidColor} : undefined}>{licEnd.toLocaleDateString()}</span>
|
||||
</div>
|
||||
{trialText && <div>{trialText}</div>}
|
||||
</TopBlock>
|
||||
);
|
||||
})();
|
||||
|
||||
// Limits block
|
||||
const limitTitle = isUsersModel ? 'Users limit' : 'Connections limit';
|
||||
const limitsBlock = (
|
||||
<TopBlock title={limitTitle}>
|
||||
<div>Editors: {limitEdit}</div>
|
||||
<div>Live Viewer: {limitView}</div>
|
||||
</TopBlock>
|
||||
);
|
||||
|
||||
const valueEdit = licenseInfo.connections - (quota.edit.connectionsCount || 0);
|
||||
const valueView = licenseInfo.connectionsView - (quota.view.connectionsCount || 0);
|
||||
const editor = [
|
||||
[quota.edit.connectionsCount || 0, ''],
|
||||
[valueEdit, valueEdit > licenseInfo.connections * 0.1 ? 'normal' : 'critical']
|
||||
];
|
||||
const viewer = [
|
||||
[quota.view.connectionsCount || 0, ''],
|
||||
[valueView, valueView > licenseInfo.connectionsView * 0.1 ? 'normal' : 'critical']
|
||||
];
|
||||
const desc = ['Active', 'Remaining'];
|
||||
// Current activity/usage table
|
||||
const currentTable = useMemo(() => {
|
||||
if (isUsersModel) {
|
||||
// Users model
|
||||
const days = parseInt(licenseInfo.usersExpire / SECONDS_PER_DAY, 10) || 1;
|
||||
const qEditUnique = quota?.edit?.usersCount?.unique || 0;
|
||||
const qEditAnon = quota?.edit?.usersCount?.anonymous || 0;
|
||||
const qViewUnique = quota?.view?.usersCount?.unique || 0;
|
||||
const qViewAnon = quota?.view?.usersCount?.anonymous || 0;
|
||||
|
||||
const peaksDesc = ['Last Hour', '24 Hours', 'Week', 'Month'];
|
||||
const peaksEditor = ['hour', 'day', 'week', 'month'].map(k => [connectionsStat?.[k]?.edit?.max || 0]);
|
||||
const peaksViewer = ['hour', 'day', 'week', 'month'].map(k => [connectionsStat?.[k]?.liveview?.max || 0]);
|
||||
const avrEditor = ['hour', 'day', 'week', 'month'].map(k => [connectionsStat?.[k]?.edit?.avr || 0]);
|
||||
const avrViewer = ['hour', 'day', 'week', 'month'].map(k => [connectionsStat?.[k]?.liveview?.avr || 0]);
|
||||
const remainingEdit = limitEdit - qEditUnique;
|
||||
const remainingView = limitView - qViewUnique;
|
||||
|
||||
const editor = [
|
||||
[qEditUnique, ''],
|
||||
[qEditUnique - qEditAnon, ''],
|
||||
[qEditAnon, ''],
|
||||
[remainingEdit, getCriticalStatus(remainingEdit, limitEdit)]
|
||||
];
|
||||
const viewer = [
|
||||
[qViewUnique, ''],
|
||||
[qViewUnique - qViewAnon, ''],
|
||||
[qViewAnon, ''],
|
||||
[remainingView, getCriticalStatus(remainingView, limitView)]
|
||||
];
|
||||
const desc = ['Active', 'Internal', 'External', 'Remaining'];
|
||||
return (
|
||||
<InfoTable
|
||||
mode={mode}
|
||||
caption={`User activity in the last ${days} ${days > 1 ? 'days' : 'day'}`}
|
||||
editor={editor}
|
||||
viewer={viewer}
|
||||
desc={desc}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Connections model
|
||||
const activeEdit = quota?.edit?.connectionsCount || 0;
|
||||
const activeView = quota?.view?.connectionsCount || 0;
|
||||
const remainingEdit = limitEdit - activeEdit;
|
||||
const remainingView = limitView - activeView;
|
||||
const editor = [
|
||||
[activeEdit, ''],
|
||||
[remainingEdit, getCriticalStatus(remainingEdit, limitEdit)]
|
||||
];
|
||||
const viewer = [
|
||||
[activeView, ''],
|
||||
[remainingView, getCriticalStatus(remainingView, limitView)]
|
||||
];
|
||||
const desc = ['Active', 'Remaining'];
|
||||
return <InfoTable mode={mode} caption='Current connections' editor={editor} viewer={viewer} desc={desc} />;
|
||||
}, [isUsersModel, licenseInfo, quota, limitEdit, limitView, mode]);
|
||||
|
||||
// Peaks and Averages (only for connections model)
|
||||
const peaksAverage = useMemo(() => {
|
||||
if (isUsersModel) return null;
|
||||
|
||||
const editorPeaks = [];
|
||||
const viewerPeaks = [];
|
||||
const editorAvr = [];
|
||||
const viewerAvr = [];
|
||||
|
||||
TIME_PERIODS.forEach((k, index) => {
|
||||
const item = connectionsStat?.[k];
|
||||
if (item?.edit) {
|
||||
let value = item.edit.max || 0;
|
||||
editorPeaks[index] = [value, value >= limitEdit ? 'critical' : ''];
|
||||
value = item.edit.avr || 0;
|
||||
editorAvr[index] = [value, value >= limitEdit ? 'critical' : ''];
|
||||
}
|
||||
if (item?.liveview) {
|
||||
let value = item.liveview.max || 0;
|
||||
viewerPeaks[index] = [value, value >= limitView ? 'critical' : ''];
|
||||
value = item.liveview.avr || 0;
|
||||
viewerAvr[index] = [value, value >= limitView ? 'critical' : ''];
|
||||
}
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<InfoTable mode={mode} caption='Peaks' editor={editorPeaks} viewer={viewerPeaks} desc={TIME_PERIOD_LABELS} />
|
||||
<InfoTable mode={mode} caption='Average' editor={editorAvr} viewer={viewerAvr} desc={TIME_PERIOD_LABELS} />
|
||||
</>
|
||||
);
|
||||
}, [isUsersModel, connectionsStat, limitEdit, limitView, mode]);
|
||||
// MonthlyStatistics moved to ./MonthlyStatistics for clarity and to keep this file concise
|
||||
|
||||
// After hooks and memos: show loading/error states
|
||||
if (error) {
|
||||
return <div style={{color: 'red'}}>Error: {error.message}</div>;
|
||||
}
|
||||
if (isLoading || !data) {
|
||||
return <div>Please, wait...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.topRow}>
|
||||
{buildBlock}
|
||||
{licenseBlock}
|
||||
{connectionsBlock}
|
||||
{limitsBlock}
|
||||
</div>
|
||||
<InfoTable caption='Current connections' editor={editor} viewer={viewer} desc={desc} />
|
||||
<InfoTable caption='Peaks' editor={peaksEditor} viewer={peaksViewer} desc={peaksDesc} />
|
||||
<InfoTable caption='Average' editor={avrEditor} viewer={avrViewer} desc={peaksDesc} />
|
||||
|
||||
<ModeSwitcher mode={mode} setMode={setMode} />
|
||||
|
||||
{currentTable}
|
||||
{peaksAverage}
|
||||
{isUsersModel && <MonthlyStatistics byMonth={quota?.byMonth} mode={mode} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,3 +3,18 @@
|
||||
margin-bottom: 24px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.modeBar {
|
||||
margin: 8px 0 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.modeLink {
|
||||
cursor: pointer;
|
||||
padding: 0 6px;
|
||||
}
|
||||
.modeSeparator {
|
||||
color: #777;
|
||||
}
|
||||
.current {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
46
tests/fixtures/README.md
vendored
Normal file
46
tests/fixtures/README.md
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
# info.json Fixtures for Rendering Tests
|
||||
|
||||
This directory contains sample `info.json` payloads that exercise different rendering paths in:
|
||||
|
||||
- Static page: `branding/info/index.html`
|
||||
- React AdminPanel: `AdminPanel/client/src/components/Statistics/`
|
||||
|
||||
Each file is self-contained and adheres to the server `info.json` schema used by the UI.
|
||||
|
||||
## Automatic Fixture Cycling
|
||||
|
||||
To enable automatic cycling through fixtures on each request, add this code to your `licenseInfo` function:
|
||||
|
||||
```javascript
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Request counter for cycling through fixtures (persistent across calls)
|
||||
licenseInfo.requestCounter = (licenseInfo.requestCounter || 0) + 1;
|
||||
licenseInfo.fixtureFiles = licenseInfo.fixtureFiles || [];
|
||||
|
||||
// Load fixture files list on first call
|
||||
if (licenseInfo.fixtureFiles.length === 0) {
|
||||
try {
|
||||
const fixturesDir = path.join(__dirname, '../../../tests/fixtures/info');
|
||||
const files = fs.readdirSync(fixturesDir);
|
||||
licenseInfo.fixtureFiles = files.filter(file => file.endsWith('.json'));
|
||||
} catch (e) {
|
||||
// If fixtures directory doesn't exist, continue with normal flow
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle through fixtures on every request
|
||||
if (licenseInfo.fixtureFiles.length > 0) {
|
||||
const fixtureIndex = (licenseInfo.requestCounter - 1) % licenseInfo.fixtureFiles.length;
|
||||
const fixturePath = path.join(__dirname, '../../../tests/fixtures/info', licenseInfo.fixtureFiles[fixtureIndex]);
|
||||
try {
|
||||
const fixtureData = JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
|
||||
return res.json(fixtureData);
|
||||
} catch (e) {
|
||||
// If fixture fails to load, continue with normal flow
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Files
|
||||
42
tests/fixtures/info/connections_basic.json
vendored
Normal file
42
tests/fixtures/info/connections_basic.json
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"licenseInfo": {
|
||||
"packageType": 1,
|
||||
"buildDate": "2025-08-20T00:00:00Z",
|
||||
"mode": 4,
|
||||
"endDate": "2026-08-20T00:00:00Z",
|
||||
"type": 0,
|
||||
"startDate": "2025-08-01T00:00:00Z",
|
||||
"connections": 50,
|
||||
"connectionsView": 30,
|
||||
"usersCount": 0,
|
||||
"usersViewCount": 0
|
||||
},
|
||||
"serverInfo": {
|
||||
"buildVersion": "8.2",
|
||||
"buildNumber": "1234",
|
||||
"date": "2025-09-05T12:00:00Z"
|
||||
},
|
||||
"quota": {
|
||||
"edit": {"connectionsCount": 20},
|
||||
"view": {"connectionsCount": 28},
|
||||
"byMonth": []
|
||||
},
|
||||
"connectionsStat": {
|
||||
"hour": {
|
||||
"edit": {"max": 52, "avr": 40},
|
||||
"liveview": {"max": 29, "avr": 27}
|
||||
},
|
||||
"day": {
|
||||
"edit": {"max": 45, "avr": 30},
|
||||
"liveview": {"max": 30, "avr": 22}
|
||||
},
|
||||
"week": {
|
||||
"edit": {"max": 50, "avr": 35},
|
||||
"liveview": {"max": 31, "avr": 26}
|
||||
},
|
||||
"month": {
|
||||
"edit": {"max": 40, "avr": 28},
|
||||
"liveview": {"max": 25, "avr": 20}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
tests/fixtures/info/connections_critical_remaining.json
vendored
Normal file
25
tests/fixtures/info/connections_critical_remaining.json
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"licenseInfo": {
|
||||
"packageType": 1,
|
||||
"buildDate": "2025-08-10T00:00:00Z",
|
||||
"mode": 4,
|
||||
"endDate": "2026-08-10T00:00:00Z",
|
||||
"type": 0,
|
||||
"startDate": "2025-08-01T00:00:00Z",
|
||||
"connections": 20,
|
||||
"connectionsView": 10,
|
||||
"usersCount": 0,
|
||||
"usersViewCount": 0
|
||||
},
|
||||
"serverInfo": {
|
||||
"buildVersion": "8.2",
|
||||
"buildNumber": "1236",
|
||||
"date": "2025-09-05T12:00:00Z"
|
||||
},
|
||||
"quota": {
|
||||
"edit": {"connectionsCount": 19},
|
||||
"view": {"connectionsCount": 9},
|
||||
"byMonth": []
|
||||
},
|
||||
"connectionsStat": {}
|
||||
}
|
||||
32
tests/fixtures/info/connections_missing_periods.json
vendored
Normal file
32
tests/fixtures/info/connections_missing_periods.json
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"licenseInfo": {
|
||||
"packageType": 1,
|
||||
"buildDate": "2025-08-15T00:00:00Z",
|
||||
"mode": 4,
|
||||
"endDate": "2026-08-15T00:00:00Z",
|
||||
"type": 0,
|
||||
"startDate": "2025-08-01T00:00:00Z",
|
||||
"connections": 40,
|
||||
"connectionsView": 25,
|
||||
"usersCount": 0,
|
||||
"usersViewCount": 0
|
||||
},
|
||||
"serverInfo": {
|
||||
"buildVersion": "8.2",
|
||||
"buildNumber": "1235",
|
||||
"date": "2025-09-05T12:00:00Z"
|
||||
},
|
||||
"quota": {
|
||||
"edit": {"connectionsCount": 12},
|
||||
"view": {"connectionsCount": 9},
|
||||
"byMonth": []
|
||||
},
|
||||
"connectionsStat": {
|
||||
"hour": {
|
||||
"edit": {"max": 10, "avr": 8}
|
||||
},
|
||||
"day": {
|
||||
"liveview": {"max": 20, "avr": 12}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
tests/fixtures/info/developer_edition_users.json
vendored
Normal file
35
tests/fixtures/info/developer_edition_users.json
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"licenseInfo": {
|
||||
"packageType": 2,
|
||||
"buildDate": "2025-08-18T00:00:00Z",
|
||||
"mode": 4,
|
||||
"endDate": "2026-08-18T00:00:00Z",
|
||||
"type": 0,
|
||||
"startDate": "2025-08-01T00:00:00Z",
|
||||
"usersCount": 200,
|
||||
"usersViewCount": 150,
|
||||
"usersExpire": 2592000
|
||||
},
|
||||
"serverInfo": {
|
||||
"buildVersion": "8.2",
|
||||
"buildNumber": "2007",
|
||||
"date": "2025-09-05T12:00:00Z"
|
||||
},
|
||||
"quota": {
|
||||
"edit": {"usersCount": {"unique": 120, "anonymous": 30}},
|
||||
"view": {"usersCount": {"unique": 100, "anonymous": 20}},
|
||||
"byMonth": [
|
||||
{
|
||||
"date": "2025-06-01T00:00:00Z",
|
||||
"users": {"a": {}, "b": {}, "c": {"anonym": true}},
|
||||
"usersView": {"d": {}, "e": {"anonym": true}}
|
||||
},
|
||||
{
|
||||
"date": "2025-07-01T00:00:00Z",
|
||||
"users": {"f": {}, "g": {"anonym": true}},
|
||||
"usersView": {"h": {}, "i": {}, "j": {"anonym": true}}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connectionsStat": {}
|
||||
}
|
||||
27
tests/fixtures/info/open_source_connection.json
vendored
Normal file
27
tests/fixtures/info/open_source_connection.json
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"licenseInfo": {
|
||||
"packageType": 0,
|
||||
"buildDate": "2025-08-12T00:00:00Z",
|
||||
"mode": 4,
|
||||
"endDate": "2026-08-12T00:00:00Z",
|
||||
"type": 0,
|
||||
"startDate": "2025-08-01T00:00:00Z",
|
||||
"connections": 15,
|
||||
"connectionsView": 10,
|
||||
"usersCount": 0,
|
||||
"usersViewCount": 0
|
||||
},
|
||||
"serverInfo": {
|
||||
"buildVersion": "8.2",
|
||||
"buildNumber": "1237",
|
||||
"date": "2025-09-05T12:00:00Z"
|
||||
},
|
||||
"quota": {
|
||||
"edit": {"connectionsCount": 5},
|
||||
"view": {"connectionsCount": 3},
|
||||
"byMonth": []
|
||||
},
|
||||
"connectionsStat": {
|
||||
"hour": {"edit": {"max": 12, "avr": 7}, "liveview": {"max": 9, "avr": 5}}
|
||||
}
|
||||
}
|
||||
35
tests/fixtures/info/users_basic.json
vendored
Normal file
35
tests/fixtures/info/users_basic.json
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"licenseInfo": {
|
||||
"packageType": 1,
|
||||
"buildDate": "2025-08-01T00:00:00Z",
|
||||
"mode": 4,
|
||||
"endDate": "2026-08-01T00:00:00Z",
|
||||
"type": 0,
|
||||
"startDate": "2025-08-01T00:00:00Z",
|
||||
"usersCount": 100,
|
||||
"usersViewCount": 60,
|
||||
"usersExpire": 2592000
|
||||
},
|
||||
"serverInfo": {
|
||||
"buildVersion": "8.2",
|
||||
"buildNumber": "2001",
|
||||
"date": "2025-09-05T12:00:00Z"
|
||||
},
|
||||
"quota": {
|
||||
"edit": {"usersCount": {"unique": 55, "anonymous": 5}},
|
||||
"view": {"usersCount": {"unique": 45, "anonymous": 2}},
|
||||
"byMonth": [
|
||||
{
|
||||
"date": "2025-06-01T00:00:00Z",
|
||||
"users": {"1": {}, "2": {"anonym": true}, "3": {}},
|
||||
"usersView": {"10": {}, "11": {"anonym": true}}
|
||||
},
|
||||
{
|
||||
"date": "2025-07-01T00:00:00Z",
|
||||
"users": {"4": {}, "5": {}, "6": {"anonym": true}},
|
||||
"usersView": {"12": {}, "13": {"anonym": true}, "14": {"anonym": true}}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connectionsStat": {}
|
||||
}
|
||||
24
tests/fixtures/info/users_critical_remaining.json
vendored
Normal file
24
tests/fixtures/info/users_critical_remaining.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"licenseInfo": {
|
||||
"packageType": 1,
|
||||
"buildDate": "2025-08-05T00:00:00Z",
|
||||
"mode": 4,
|
||||
"endDate": "2026-08-05T00:00:00Z",
|
||||
"type": 0,
|
||||
"startDate": "2025-08-01T00:00:00Z",
|
||||
"usersCount": 10,
|
||||
"usersViewCount": 5,
|
||||
"usersExpire": 604800
|
||||
},
|
||||
"serverInfo": {
|
||||
"buildVersion": "8.2",
|
||||
"buildNumber": "2002",
|
||||
"date": "2025-09-05T12:00:00Z"
|
||||
},
|
||||
"quota": {
|
||||
"edit": {"usersCount": {"unique": 10, "anonymous": 2}},
|
||||
"view": {"usersCount": {"unique": 5, "anonymous": 1}},
|
||||
"byMonth": []
|
||||
},
|
||||
"connectionsStat": {}
|
||||
}
|
||||
24
tests/fixtures/info/users_license_invalid_type.json
vendored
Normal file
24
tests/fixtures/info/users_license_invalid_type.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"licenseInfo": {
|
||||
"packageType": 1,
|
||||
"buildDate": "2025-08-01T00:00:00Z",
|
||||
"mode": 4,
|
||||
"endDate": "2025-12-01T00:00:00Z",
|
||||
"type": 2,
|
||||
"startDate": "2025-08-01T00:00:00Z",
|
||||
"usersCount": 100,
|
||||
"usersViewCount": 80,
|
||||
"usersExpire": 2592000
|
||||
},
|
||||
"serverInfo": {
|
||||
"buildVersion": "8.2",
|
||||
"buildNumber": "2005",
|
||||
"date": "2025-09-05T12:00:00Z"
|
||||
},
|
||||
"quota": {
|
||||
"edit": {"usersCount": {"unique": 50, "anonymous": 10}},
|
||||
"view": {"usersCount": {"unique": 40, "anonymous": 5}},
|
||||
"byMonth": []
|
||||
},
|
||||
"connectionsStat": {}
|
||||
}
|
||||
24
tests/fixtures/info/users_no_license.json
vendored
Normal file
24
tests/fixtures/info/users_no_license.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"licenseInfo": {
|
||||
"packageType": 1,
|
||||
"buildDate": "2025-08-01T00:00:00Z",
|
||||
"mode": 0,
|
||||
"endDate": null,
|
||||
"type": 0,
|
||||
"startDate": "2025-08-01T00:00:00Z",
|
||||
"usersCount": 50,
|
||||
"usersViewCount": 40,
|
||||
"usersExpire": 1209600
|
||||
},
|
||||
"serverInfo": {
|
||||
"buildVersion": "8.2",
|
||||
"buildNumber": "2003",
|
||||
"date": "2025-09-05T12:00:00Z"
|
||||
},
|
||||
"quota": {
|
||||
"edit": {"usersCount": {"unique": 5, "anonymous": 1}},
|
||||
"view": {"usersCount": {"unique": 2, "anonymous": 0}},
|
||||
"byMonth": []
|
||||
},
|
||||
"connectionsStat": {}
|
||||
}
|
||||
24
tests/fixtures/info/users_trial_limited_start_critical.json
vendored
Normal file
24
tests/fixtures/info/users_trial_limited_start_critical.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"licenseInfo": {
|
||||
"packageType": 1,
|
||||
"buildDate": "2025-09-01T00:00:00Z",
|
||||
"mode": 5,
|
||||
"endDate": "2026-09-01T00:00:00Z",
|
||||
"type": 16,
|
||||
"startDate": "2025-09-10T00:00:00Z",
|
||||
"usersCount": 100,
|
||||
"usersViewCount": 100,
|
||||
"usersExpire": 2592000
|
||||
},
|
||||
"serverInfo": {
|
||||
"buildVersion": "8.2",
|
||||
"buildNumber": "2004",
|
||||
"date": "2025-09-05T12:00:00Z"
|
||||
},
|
||||
"quota": {
|
||||
"edit": {"usersCount": {"unique": 1, "anonymous": 0}},
|
||||
"view": {"usersCount": {"unique": 1, "anonymous": 0}},
|
||||
"byMonth": []
|
||||
},
|
||||
"connectionsStat": {}
|
||||
}
|
||||
24
tests/fixtures/info/users_updates_unavailable.json
vendored
Normal file
24
tests/fixtures/info/users_updates_unavailable.json
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"licenseInfo": {
|
||||
"packageType": 1,
|
||||
"buildDate": "2025-07-01T00:00:00Z",
|
||||
"mode": 0,
|
||||
"endDate": "2025-09-01T00:00:00Z",
|
||||
"type": 0,
|
||||
"startDate": "2025-07-01T00:00:00Z",
|
||||
"usersCount": 100,
|
||||
"usersViewCount": 80,
|
||||
"usersExpire": 2592000
|
||||
},
|
||||
"serverInfo": {
|
||||
"buildVersion": "8.2",
|
||||
"buildNumber": "2006",
|
||||
"date": "2025-09-10T12:00:00Z"
|
||||
},
|
||||
"quota": {
|
||||
"edit": {"usersCount": {"unique": 50, "anonymous": 5}},
|
||||
"view": {"usersCount": {"unique": 40, "anonymous": 4}},
|
||||
"byMonth": []
|
||||
},
|
||||
"connectionsStat": {}
|
||||
}
|
||||
Reference in New Issue
Block a user