mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-02-10 18:05:07 +08:00
Merge branch 'release/v9.1.0' into feature/admin-panel2
This commit is contained in:
@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="/images/favicon.ico" />
|
<link rel="icon" href="images/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="Document Server Admin Panel" />
|
<meta name="description" content="Document Server Admin Panel" />
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL ?? '';
|
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL ?? '';
|
||||||
|
|
||||||
export const fetchStatistics = async () => {
|
export const fetchStatistics = async () => {
|
||||||
const response = await fetch(`${BACKEND_URL}/info/info.json`);
|
const response = await fetch(`${BACKEND_URL}/api/v1/admin/stat`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch statistics');
|
throw new Error('Failed to fetch statistics');
|
||||||
}
|
}
|
||||||
@ -9,7 +9,7 @@ export const fetchStatistics = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchConfiguration = async () => {
|
export const fetchConfiguration = async () => {
|
||||||
const response = await fetch(`${BACKEND_URL}/info/config`, {
|
const response = await fetch(`${BACKEND_URL}/api/v1/admin/config`, {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -19,7 +19,7 @@ export const fetchConfiguration = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchConfigurationSchema = async () => {
|
export const fetchConfigurationSchema = async () => {
|
||||||
const response = await fetch(`${BACKEND_URL}/info/config/schema`, {
|
const response = await fetch(`${BACKEND_URL}/api/v1/admin/config/schema`, {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -29,7 +29,7 @@ export const fetchConfigurationSchema = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateConfiguration = async configData => {
|
export const updateConfiguration = async configData => {
|
||||||
const response = await fetch(`${BACKEND_URL}/info/config`, {
|
const response = await fetch(`${BACKEND_URL}/api/v1/admin/config`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@ -59,7 +59,7 @@ export const updateConfiguration = async configData => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchCurrentUser = async () => {
|
export const fetchCurrentUser = async () => {
|
||||||
const response = await fetch(`${BACKEND_URL}/info/adminpanel/me`, {
|
const response = await fetch(`${BACKEND_URL}/api/v1/admin/me`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include' // Include cookies in the request
|
credentials: 'include' // Include cookies in the request
|
||||||
});
|
});
|
||||||
@ -75,7 +75,7 @@ export const fetchCurrentUser = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const login = async ({tenantName, secret}) => {
|
export const login = async ({tenantName, secret}) => {
|
||||||
const response = await fetch(`${BACKEND_URL}/info/adminpanel/login`, {
|
const response = await fetch(`${BACKEND_URL}/api/v1/admin/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@ -95,7 +95,7 @@ export const login = async ({tenantName, secret}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const logout = async () => {
|
export const logout = async () => {
|
||||||
const response = await fetch(`${BACKEND_URL}/info/adminpanel/logout`, {
|
const response = await fetch(`${BACKEND_URL}/api/v1/admin/logout`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
import styles from './StatisticsInfoTable.module.scss';
|
|
||||||
|
|
||||||
function StatisticsInfoTable({caption, editor, viewer, desc}) {
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
{caption && <div className={styles.sectionHeader}>{caption}</div>}
|
|
||||||
<div className={styles.sectionLabel}>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]}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className={styles.row}>
|
|
||||||
{desc.map((d, i) => (
|
|
||||||
<div key={i} className={styles.labelCell}>
|
|
||||||
{d}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className={styles.sectionLabel}>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]}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className={styles.row}>
|
|
||||||
{desc.map((d, i) => (
|
|
||||||
<div key={i} className={styles.labelCell}>
|
|
||||||
{d}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StatisticsInfoTable;
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
.container {
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionHeader {
|
|
||||||
background: #f5f5f5;
|
|
||||||
font-weight: 400;
|
|
||||||
padding: 6px 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.valueCell {
|
|
||||||
font-size: 22px;
|
|
||||||
padding: 8px 0;
|
|
||||||
width: 25%;
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labelCell {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #555;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
width: 25%;
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionLabel {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 8px 0 0 10px;
|
|
||||||
|
|
||||||
&:not(:first-child) {
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
margin: 2px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remainingValue {
|
|
||||||
color: rgb(1, 125, 28);
|
|
||||||
}
|
|
||||||
@ -4,7 +4,7 @@ import SecuritySettings from '../pages/SecuritySettings/SecuritySettings';
|
|||||||
import EmailConfig from '../pages/EmailConfig/EmailConfig';
|
import EmailConfig from '../pages/EmailConfig/EmailConfig';
|
||||||
import FileLimits from '../pages/FileLimits/FileLimits';
|
import FileLimits from '../pages/FileLimits/FileLimits';
|
||||||
import RequestFiltering from '../pages/RequestFiltering/RequestFiltering';
|
import RequestFiltering from '../pages/RequestFiltering/RequestFiltering';
|
||||||
import Statistics from '../pages/Statistics/Statistics';
|
import Statistics from '../pages/Statistics';
|
||||||
|
|
||||||
// Generic component factory function for pages not yet implemented
|
// Generic component factory function for pages not yet implemented
|
||||||
const createMockComponent = label => () => <div>{label} Component</div>;
|
const createMockComponent = label => () => <div>{label} Component</div>;
|
||||||
|
|||||||
64
AdminPanel/client/src/pages/Statistics/InfoTable/index.js
Normal file
64
AdminPanel/client/src/pages/Statistics/InfoTable/index.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>}
|
||||||
|
|
||||||
|
{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 className={styles.row}>
|
||||||
|
{[0, 1, 2, 3].map(i => (
|
||||||
|
<div key={i} className={styles.labelCell}>
|
||||||
|
{desc[i] || ''}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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 className={styles.row}>
|
||||||
|
{[0, 1, 2, 3].map(i => (
|
||||||
|
<div key={i} className={styles.labelCell}>
|
||||||
|
{desc[i] || ''}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
.container {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
.sectionHeader {
|
||||||
|
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: 26px;
|
||||||
|
line-height: 28px;
|
||||||
|
padding: 8px 0;
|
||||||
|
width: 25%;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
.labelCell {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #555;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
width: 25%;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
.editorsLabel,
|
||||||
|
.viewerLabel {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewerLabel {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
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/pages/Statistics/ModeSwitcher.js
Normal file
28
AdminPanel/client/src/pages/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>
|
||||||
|
);
|
||||||
|
}
|
||||||
86
AdminPanel/client/src/pages/Statistics/MonthlyStatistics.js
Normal file
86
AdminPanel/client/src/pages/Statistics/MonthlyStatistics.js
Normal file
@ -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,81 +0,0 @@
|
|||||||
import {useQuery} from '@tanstack/react-query';
|
|
||||||
import {fetchStatistics} from '../../api';
|
|
||||||
import StatisticsTopBlock from '../../components/StatisticsTopBlock/StatisticsTopBlock';
|
|
||||||
import StatisticsInfoTable from '../../components/StatisticsInfoTable/StatisticsInfoTable';
|
|
||||||
import styles from './Statistics.module.scss';
|
|
||||||
|
|
||||||
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 {licenseInfo, quota, connectionsStat, serverInfo} = data;
|
|
||||||
|
|
||||||
const buildDate = licenseInfo.buildDate ? new Date(licenseInfo.buildDate).toLocaleDateString() : '';
|
|
||||||
|
|
||||||
const buildBlock = (
|
|
||||||
<StatisticsTopBlock title='Build'>
|
|
||||||
<div>Type: {licenseInfo.packageType === 0 ? 'Open source' : licenseInfo.packageType === 1 ? 'Enterprise Edition' : 'Developer Edition'}</div>
|
|
||||||
<div>
|
|
||||||
Version: {serverInfo.buildVersion}.{serverInfo.buildNumber}
|
|
||||||
</div>
|
|
||||||
<div>Release date: {buildDate}</div>
|
|
||||||
</StatisticsTopBlock>
|
|
||||||
);
|
|
||||||
|
|
||||||
const licenseBlock = (
|
|
||||||
<StatisticsTopBlock title='License'>
|
|
||||||
{licenseInfo.startDate === null ? (
|
|
||||||
'No license'
|
|
||||||
) : (
|
|
||||||
<div>Start date: {licenseInfo.startDate ? new Date(licenseInfo.startDate).toLocaleDateString() : ''}</div>
|
|
||||||
)}
|
|
||||||
</StatisticsTopBlock>
|
|
||||||
);
|
|
||||||
|
|
||||||
const connectionsBlock = (
|
|
||||||
<StatisticsTopBlock title='Connections limit'>
|
|
||||||
<div>Editors: {licenseInfo.connections}</div>
|
|
||||||
<div>Live Viewer: {licenseInfo.connectionsView}</div>
|
|
||||||
</StatisticsTopBlock>
|
|
||||||
);
|
|
||||||
|
|
||||||
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'];
|
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={styles.topRow}>
|
|
||||||
{buildBlock}
|
|
||||||
{licenseBlock}
|
|
||||||
{connectionsBlock}
|
|
||||||
</div>
|
|
||||||
<StatisticsInfoTable caption='Current connections' editor={editor} viewer={viewer} desc={desc} />
|
|
||||||
<StatisticsInfoTable caption='Peaks' editor={peaksEditor} viewer={peaksViewer} desc={peaksDesc} />
|
|
||||||
<StatisticsInfoTable caption='Average' editor={avrEditor} viewer={avrViewer} desc={peaksDesc} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Statistics;
|
|
||||||
@ -3,3 +3,18 @@
|
|||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modeBar {
|
||||||
|
margin: 8px 0 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.modeLink {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
.modeSeparator {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
.current {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|||||||
10
AdminPanel/client/src/pages/Statistics/TopBlock/index.js
Normal file
10
AdminPanel/client/src/pages/Statistics/TopBlock/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
export default function TopBlock({title, children}) {
|
||||||
|
return (
|
||||||
|
<div className={styles.block}>
|
||||||
|
<div className={styles.title}>{title}</div>
|
||||||
|
<div className={styles.content}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
.block {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 18px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
padding: 0 0 5px 10px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
256
AdminPanel/client/src/pages/Statistics/index.js
Normal file
256
AdminPanel/client/src/pages/Statistics/index.js
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
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, fetchConfiguration} 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
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch configuration to display DB info
|
||||||
|
const {data: configData} = useQuery({
|
||||||
|
queryKey: ['configuration'],
|
||||||
|
queryFn: fetchConfiguration
|
||||||
|
});
|
||||||
|
|
||||||
|
const [mode, setMode] = useState(() => {
|
||||||
|
try {
|
||||||
|
const saved = window.localStorage?.getItem('server-info-display-mode');
|
||||||
|
return saved || 'all';
|
||||||
|
} catch {
|
||||||
|
return 'all';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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: {packageTypeLabel}</div>
|
||||||
|
<div>
|
||||||
|
Version: {serverInfo.buildVersion}.{serverInfo.buildNumber}
|
||||||
|
</div>
|
||||||
|
<div>Release date: {buildDate}</div>
|
||||||
|
</TopBlock>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render database info block
|
||||||
|
* @param {object|null} sql - services.CoAuthoring.sql config
|
||||||
|
* @returns {JSX.Element|null}
|
||||||
|
*/
|
||||||
|
const renderDatabaseBlock = sql => {
|
||||||
|
if (!sql) return null;
|
||||||
|
return (
|
||||||
|
<TopBlock title='Database'>
|
||||||
|
<div>Type: {sql.type}</div>
|
||||||
|
<div>Host: {sql.dbHost}</div>
|
||||||
|
<div>Port: {sql.dbPort}</div>
|
||||||
|
<div>Name: {sql.dbName}</div>
|
||||||
|
</TopBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 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}
|
||||||
|
{limitsBlock}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renderDatabaseBlock(configData?.services?.CoAuthoring?.sql)}
|
||||||
|
|
||||||
|
<ModeSwitcher mode={mode} setMode={setMode} />
|
||||||
|
|
||||||
|
{currentTable}
|
||||||
|
{peaksAverage}
|
||||||
|
{isUsersModel && <MonthlyStatistics byMonth={quota?.byMonth} mode={mode} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
AdminPanel/client/src/pages/Statistics/styles.module.css
Normal file
21
AdminPanel/client/src/pages/Statistics/styles.module.css
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.topRow {
|
||||||
|
display: flex;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
@ -8,7 +8,8 @@ module.exports = {
|
|||||||
output: {
|
output: {
|
||||||
filename: 'main.[contenthash].js',
|
filename: 'main.[contenthash].js',
|
||||||
path: path.resolve(__dirname, 'build'),
|
path: path.resolve(__dirname, 'build'),
|
||||||
publicPath: '/'
|
// Use relative URLs so assets load under any prefix (e.g., /admin)
|
||||||
|
publicPath: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
devServer: {
|
devServer: {
|
||||||
|
|||||||
@ -78,24 +78,31 @@ const corsWithCredentials = cors({
|
|||||||
|
|
||||||
operationContext.global.logger.warn('AdminPanel server starting...');
|
operationContext.global.logger.warn('AdminPanel server starting...');
|
||||||
|
|
||||||
app.use('/info/config', corsWithCredentials, utils.checkClientIp, configRouter);
|
app.use('/api/v1/admin/config', corsWithCredentials, utils.checkClientIp, configRouter);
|
||||||
app.use('/info/adminpanel', corsWithCredentials, utils.checkClientIp, adminpanelRouter);
|
app.use('/api/v1/admin', corsWithCredentials, utils.checkClientIp, adminpanelRouter);
|
||||||
// Shared Info router (provides /info.json)
|
app.get('/api/v1/admin/stat', corsWithCredentials, utils.checkClientIp, infoRouter.licenseInfo);
|
||||||
app.use('/info', infoRouter());
|
|
||||||
|
|
||||||
// todo config or _dirname. Serve AdminPanel client build as static assets
|
// Serve AdminPanel client build as static assets.
|
||||||
|
// Use admin prefix in production (or ADMIN_PREFIX env), no prefix locally.
|
||||||
const clientBuildPath = path.resolve('client/build');
|
const clientBuildPath = path.resolve('client/build');
|
||||||
app.use(express.static(clientBuildPath));
|
// Normalize admin prefix: default '/admin' in production, '' otherwise.
|
||||||
|
const rawAdminPrefix = process.env.ADMIN_PREFIX || (process.env.NODE_ENV === 'production' ? '/admin' : '');
|
||||||
|
const adminPrefix = rawAdminPrefix && rawAdminPrefix !== '/' ? rawAdminPrefix : '';
|
||||||
|
app.use(adminPrefix || '/', express.static(clientBuildPath));
|
||||||
|
|
||||||
function serveSpaIndex(req, res, next) {
|
function serveSpaIndex(req, res, next) {
|
||||||
if (req.path.startsWith('/info/')) return next();
|
if (req.path.startsWith('/api')) return next();
|
||||||
res.sendFile(path.join(clientBuildPath, 'index.html'));
|
res.sendFile(path.join(clientBuildPath, 'index.html'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// client SPA routes
|
// client SPA routes (prefix in prod, root locally)
|
||||||
app.get('*', serveSpaIndex);
|
if (adminPrefix) {
|
||||||
|
app.get(`${adminPrefix}/*`, serveSpaIndex);
|
||||||
|
} else {
|
||||||
|
app.get('*', serveSpaIndex);
|
||||||
|
}
|
||||||
|
|
||||||
app.use((err, req, res) => {
|
app.use((err, req, res, _next) => {
|
||||||
const ctx = new operationContext.Context();
|
const ctx = new operationContext.Context();
|
||||||
ctx.initFromRequest(req);
|
ctx.initFromRequest(req);
|
||||||
ctx.logger.error('default error handler:%s', err.stack);
|
ctx.logger.error('default error handler:%s', err.stack);
|
||||||
|
|||||||
@ -433,8 +433,7 @@
|
|||||||
},
|
},
|
||||||
"mysqlExtraOptions": {
|
"mysqlExtraOptions": {
|
||||||
"connectTimeout": 60000,
|
"connectTimeout": 60000,
|
||||||
"queryTimeout": 60000,
|
"queryTimeout": 60000
|
||||||
"autoCommit": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"redis": {
|
"redis": {
|
||||||
|
|||||||
@ -70,6 +70,36 @@
|
|||||||
"description": "HTTP error code to return when IP is not allowed"
|
"description": "HTTP error code to return when IP is not allowed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"sql": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Database connection settings for the CoAuthoring service",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"x-scope": "admin",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Database type (e.g., 'mysql', 'mariadb', 'mssql', 'postgres', 'dameng', 'oracle')",
|
||||||
|
"examples": ["postgres"]
|
||||||
|
},
|
||||||
|
"dbHost": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Database host name or IP address",
|
||||||
|
"examples": ["localhost"]
|
||||||
|
},
|
||||||
|
"dbPort": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65535,
|
||||||
|
"description": "Database TCP port",
|
||||||
|
"examples": [5432]
|
||||||
|
},
|
||||||
|
"dbName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Database name",
|
||||||
|
"examples": ["onlyoffice"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,6 +76,13 @@ Context.prototype.initFromConnection = function (conn) {
|
|||||||
const userSessionId = utils.getSessionIdByConnection(this, conn);
|
const userSessionId = utils.getSessionIdByConnection(this, conn);
|
||||||
this.init(tenant, docId || this.docId, userId || this.userId, shardKey, wopiSrc, userSessionId);
|
this.init(tenant, docId || this.docId, userId || this.userId, shardKey, wopiSrc, userSessionId);
|
||||||
};
|
};
|
||||||
|
Context.prototype.initFromConnectionRequest = function (req) {
|
||||||
|
this.initFromRequest(req);
|
||||||
|
const docIdParsed = constants.DOC_ID_SOCKET_PATTERN.exec(req.url);
|
||||||
|
if (docIdParsed && 1 < docIdParsed.length) {
|
||||||
|
this.setDocId(docIdParsed[1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
Context.prototype.initFromRequest = function (req) {
|
Context.prototype.initFromRequest = function (req) {
|
||||||
const tenant = tenantManager.getTenantByRequest(this, req);
|
const tenant = tenantManager.getTenantByRequest(this, req);
|
||||||
const shardKey = utils.getShardKeyByRequest(this, req);
|
const shardKey = utils.getShardKeyByRequest(this, req);
|
||||||
|
|||||||
@ -924,6 +924,25 @@ exports.getSessionIdByConnection = getSessionIdByConnection;
|
|||||||
exports.getShardKeyByRequest = getShardKeyByRequest;
|
exports.getShardKeyByRequest = getShardKeyByRequest;
|
||||||
exports.getWopiSrcByRequest = getWopiSrcByRequest;
|
exports.getWopiSrcByRequest = getWopiSrcByRequest;
|
||||||
exports.getSessionIdByRequest = getSessionIdByRequest;
|
exports.getSessionIdByRequest = getSessionIdByRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapt a raw Node/engine.io IncomingMessage to behave like an Express Request.
|
||||||
|
* @param {http.IncomingMessage} rawReq
|
||||||
|
* @param {Express} app
|
||||||
|
*/
|
||||||
|
exports.expressifyIncomingMessage = function (rawReq, app) {
|
||||||
|
if (!rawReq || !app?.request || rawReq.app) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.setPrototypeOf(rawReq, app.request);
|
||||||
|
rawReq.app = app;
|
||||||
|
|
||||||
|
// Initialize Express-like properties
|
||||||
|
rawReq.originalUrl = rawReq.originalUrl || rawReq.url || '/';
|
||||||
|
rawReq.query = rawReq.query || (rawReq.url ? url.parse(rawReq.url, true).query : {});
|
||||||
|
};
|
||||||
|
|
||||||
function stream2Buffer(stream) {
|
function stream2Buffer(stream) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!stream.readable) {
|
if (!stream.readable) {
|
||||||
|
|||||||
@ -1806,7 +1806,7 @@ async function encryptPasswordParams(ctx, data) {
|
|||||||
}
|
}
|
||||||
exports.encryptPasswordParams = encryptPasswordParams;
|
exports.encryptPasswordParams = encryptPasswordParams;
|
||||||
exports.getOpenFormatByEditor = getOpenFormatByEditor;
|
exports.getOpenFormatByEditor = getOpenFormatByEditor;
|
||||||
exports.install = function (server, callbackFunction) {
|
exports.install = function (server, app, callbackFunction) {
|
||||||
const io = new Server(server, cfgSocketIoConnection);
|
const io = new Server(server, cfgSocketIoConnection);
|
||||||
|
|
||||||
io.use((socket, next) => {
|
io.use((socket, next) => {
|
||||||
@ -2022,7 +2022,15 @@ exports.install = function (server, callbackFunction) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
io.engine.on('connection_error', err => {
|
io.engine.on('connection_error', err => {
|
||||||
operationContext.global.logger.warn('io.connection_error code=%s, message=%s', err.code, err.message);
|
let logger = operationContext.global.logger;
|
||||||
|
if (err.req) {
|
||||||
|
const ctx = new operationContext.Context();
|
||||||
|
// Ensure raw IncomingMessage has Express properties for consistent context init
|
||||||
|
utils.expressifyIncomingMessage(err.req, app);
|
||||||
|
ctx.initFromConnectionRequest(err.req);
|
||||||
|
logger = ctx.logger;
|
||||||
|
}
|
||||||
|
logger.warn('io.connection_error code=%s, message=%s, url=%s', err?.code, err?.message, err?.req?.url);
|
||||||
});
|
});
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
@ -34,7 +34,6 @@
|
|||||||
|
|
||||||
const mysql = require('mysql2/promise');
|
const mysql = require('mysql2/promise');
|
||||||
const connectorUtilities = require('./connectorUtilities');
|
const connectorUtilities = require('./connectorUtilities');
|
||||||
const operationContext = require('../../../Common/sources/operationContext');
|
|
||||||
const config = require('config');
|
const config = require('config');
|
||||||
|
|
||||||
const configSql = config.get('services.CoAuthoring.sql');
|
const configSql = config.get('services.CoAuthoring.sql');
|
||||||
@ -59,25 +58,9 @@ if (configuration.queryTimeout) {
|
|||||||
queryTimeout = configuration.queryTimeout;
|
queryTimeout = configuration.queryTimeout;
|
||||||
delete configuration.queryTimeout;
|
delete configuration.queryTimeout;
|
||||||
}
|
}
|
||||||
let autoCommit = false;
|
|
||||||
if (configuration.autoCommit !== undefined) {
|
|
||||||
//delete to fix issue with invalid configuration option
|
|
||||||
autoCommit = configuration.autoCommit;
|
|
||||||
delete configuration.autoCommit;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pool = mysql.createPool(configuration);
|
const pool = mysql.createPool(configuration);
|
||||||
|
|
||||||
// Set autocommit once per connection
|
|
||||||
if (autoCommit === true) {
|
|
||||||
pool.on('connection', async conn => {
|
|
||||||
conn
|
|
||||||
.promise()
|
|
||||||
.query('SET autocommit=1')
|
|
||||||
.catch(err => operationContext.global.logger.error('Failed to set autocommit=1:', err.message));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sqlQuery(ctx, sqlCommand, callbackFunction, opt_noModifyRes = false, opt_noLog = false, opt_values = []) {
|
function sqlQuery(ctx, sqlCommand, callbackFunction, opt_noModifyRes = false, opt_noLog = false, opt_values = []) {
|
||||||
return executeQuery(ctx, sqlCommand, opt_values, opt_noModifyRes, opt_noLog).then(
|
return executeQuery(ctx, sqlCommand, opt_values, opt_noModifyRes, opt_noLog).then(
|
||||||
result => callbackFunction?.(null, result),
|
result => callbackFunction?.(null, result),
|
||||||
@ -90,6 +73,12 @@ async function executeQuery(ctx, sqlCommand, values = [], noModifyRes = false, n
|
|||||||
try {
|
try {
|
||||||
connection = await pool.getConnection();
|
connection = await pool.getConnection();
|
||||||
|
|
||||||
|
// Ensure session autocommit=1 once per physical connection; avoids pool 'connection' race and per-query overhead
|
||||||
|
if (!connection.__autocommitSet) {
|
||||||
|
await connection.query('SET autocommit=1');
|
||||||
|
connection.__autocommitSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await connection.query({sql: sqlCommand, timeout: queryTimeout, values});
|
const result = await connection.query({sql: sqlCommand, timeout: queryTimeout, values});
|
||||||
|
|
||||||
let output;
|
let output;
|
||||||
|
|||||||
@ -87,25 +87,30 @@ const checkFileExpire = function (expireSeconds) {
|
|||||||
const shardKey = sqlBase.DocumentAdditional.prototype.getShardKey(expired[i].additional);
|
const shardKey = sqlBase.DocumentAdditional.prototype.getShardKey(expired[i].additional);
|
||||||
const wopiSrc = sqlBase.DocumentAdditional.prototype.getWopiSrc(expired[i].additional);
|
const wopiSrc = sqlBase.DocumentAdditional.prototype.getWopiSrc(expired[i].additional);
|
||||||
|
|
||||||
if (currentTenant !== tenant) {
|
try {
|
||||||
ctx.init(tenant, docId, ctx.userId, shardKey, wopiSrc);
|
if (currentTenant !== tenant) {
|
||||||
yield ctx.initTenantCache();
|
ctx.init(tenant, docId, ctx.userId, shardKey, wopiSrc);
|
||||||
currentTenant = tenant;
|
yield ctx.initTenantCache();
|
||||||
} else {
|
currentTenant = tenant;
|
||||||
ctx.setDocId(docId);
|
} else {
|
||||||
ctx.setShardKey(shardKey);
|
ctx.setDocId(docId);
|
||||||
ctx.setWopiSrc(wopiSrc);
|
ctx.setShardKey(shardKey);
|
||||||
}
|
ctx.setWopiSrc(wopiSrc);
|
||||||
|
|
||||||
//todo tenant
|
|
||||||
//check that no one is in the document
|
|
||||||
const editorsCount = yield docsCoServer.getEditorsCountPromise(ctx, docId);
|
|
||||||
if (0 === editorsCount) {
|
|
||||||
if (yield canvasService.cleanupCache(ctx, docId)) {
|
|
||||||
currentRemovedCount++;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ctx.logger.debug('checkFileExpire expire but presence: editorsCount = %d', editorsCount);
|
//todo tenant
|
||||||
|
//check that no one is in the document
|
||||||
|
const editorsCount = yield docsCoServer.getEditorsCountPromise(ctx, docId);
|
||||||
|
if (0 === editorsCount) {
|
||||||
|
if (yield canvasService.cleanupCache(ctx, docId)) {
|
||||||
|
currentRemovedCount++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.logger.debug('checkFileExpire expire but presence: editorsCount = %d', editorsCount);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.error('checkFileExpire file error: %s', error.stack);
|
||||||
|
// Continue processing other files
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
removedCount += currentRemovedCount;
|
removedCount += currentRemovedCount;
|
||||||
|
|||||||
@ -18,8 +18,7 @@ const cfgEditorStatStorage =
|
|||||||
// Initialize editor stat storage
|
// Initialize editor stat storage
|
||||||
const editorStatStorage = require(`../${cfgEditorStatStorage}`);
|
const editorStatStorage = require(`../${cfgEditorStatStorage}`);
|
||||||
const editorStat = new editorStatStorage.EditorStat();
|
const editorStat = new editorStatStorage.EditorStat();
|
||||||
console.error(`../${cfgEditorStatStorage}`);
|
|
||||||
console.error(editorStat);
|
|
||||||
// Constants
|
// Constants
|
||||||
const PRECISION = [
|
const PRECISION = [
|
||||||
{name: 'hour', val: ms('1h')},
|
{name: 'hour', val: ms('1h')},
|
||||||
@ -242,3 +241,5 @@ function createInfoRouter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = createInfoRouter;
|
module.exports = createInfoRouter;
|
||||||
|
// Export handler for reuse
|
||||||
|
module.exports.licenseInfo = licenseInfo;
|
||||||
|
|||||||
@ -157,7 +157,7 @@ try {
|
|||||||
// If you want to use 'development' and 'production',
|
// If you want to use 'development' and 'production',
|
||||||
// then with app.settings.env (https://github.com/strongloop/express/issues/936)
|
// then with app.settings.env (https://github.com/strongloop/express/issues/936)
|
||||||
// If error handling is needed, now it's like this https://github.com/expressjs/errorhandler
|
// If error handling is needed, now it's like this https://github.com/expressjs/errorhandler
|
||||||
docsCoServer.install(server, () => {
|
docsCoServer.install(server, app, () => {
|
||||||
operationContext.global.logger.info('Start callbackFunction');
|
operationContext.global.logger.info('Start callbackFunction');
|
||||||
|
|
||||||
server.listen(config.get('services.CoAuthoring.server.port'), () => {
|
server.listen(config.get('services.CoAuthoring.server.port'), () => {
|
||||||
|
|||||||
@ -75,13 +75,12 @@ for file in filesReplace:
|
|||||||
|
|
||||||
testDevelopVersion = sdkjsDirectory + "/.git"
|
testDevelopVersion = sdkjsDirectory + "/.git"
|
||||||
if not os.path.isdir(testDevelopVersion):
|
if not os.path.isdir(testDevelopVersion):
|
||||||
print("Not in develop version, skipping x2t cache update")
|
print("Update x2t cache...")
|
||||||
x2tDir = curDirectory + "/../FileConverter/bin"
|
x2tDir = curDirectory + "/../FileConverter/bin"
|
||||||
cur_dir = os.getcwd()
|
cur_dir = os.getcwd()
|
||||||
os.chdir(x2tDir)
|
os.chdir(x2tDir)
|
||||||
x2tBin = curDirectory + "/../FileConverter/bin/x2t"
|
|
||||||
if ("windows" == platform.system().lower()):
|
if ("windows" == platform.system().lower()):
|
||||||
subprocess.call(["x2t.exe", "-create-js-cache"], stderr=subprocess.STDOUT, shell=True)
|
subprocess.call(["x2t.exe", "-create-js-cache"], stderr=subprocess.STDOUT, shell=True)
|
||||||
else:
|
else:
|
||||||
subprocess.call("x2t -create-js-cache", stderr=subprocess.STDOUT, shell=True)
|
subprocess.call("./x2t -create-js-cache", stderr=subprocess.STDOUT, shell=True)
|
||||||
os.chdir(cur_dir)
|
os.chdir(cur_dir)
|
||||||
|
|||||||
6660
npm-shrinkwrap.json
generated
6660
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
@ -19,9 +19,9 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"express": "4.21.2",
|
"express": "4.21.2",
|
||||||
"globals": "15.12.0",
|
"globals": "15.12.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "8.0.3",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"lint-staged": "^16.1.5",
|
"lint-staged": "15.2.10",
|
||||||
"prettier": "3.4.2"
|
"prettier": "3.4.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -31,7 +31,6 @@
|
|||||||
"format:check": "prettier . --check",
|
"format:check": "prettier . --check",
|
||||||
"code:check": "run-s lint:check format:check",
|
"code:check": "run-s lint:check format:check",
|
||||||
"code:fix": "run-s lint:fix format:fix",
|
"code:fix": "run-s lint:fix format:fix",
|
||||||
"prepare": "husky",
|
|
||||||
"perf-expired": "cd ./DocService&& cross-env NODE_ENV=development-windows NODE_CONFIG_DIR=../Common/config node ../tests/perf/checkFileExpire.js",
|
"perf-expired": "cd ./DocService&& cross-env NODE_ENV=development-windows NODE_CONFIG_DIR=../Common/config node ../tests/perf/checkFileExpire.js",
|
||||||
"perf-exif": "cd ./DocService&& cross-env NODE_ENV=development-windows NODE_CONFIG_DIR=../Common/config node ../tests/perf/fixImageExifRotation.js",
|
"perf-exif": "cd ./DocService&& cross-env NODE_ENV=development-windows NODE_CONFIG_DIR=../Common/config node ../tests/perf/fixImageExifRotation.js",
|
||||||
"perf-png": "cd ./DocService&& cross-env NODE_ENV=development-windows NODE_CONFIG_DIR=../Common/config node ../tests/perf/convertImageToPng.js",
|
"perf-png": "cd ./DocService&& cross-env NODE_ENV=development-windows NODE_CONFIG_DIR=../Common/config node ../tests/perf/convertImageToPng.js",
|
||||||
@ -46,7 +45,7 @@
|
|||||||
"install:FileConverter": "npm ci --prefix ./FileConverter",
|
"install:FileConverter": "npm ci --prefix ./FileConverter",
|
||||||
"install:Metrics": "npm ci --prefix ./Metrics",
|
"install:Metrics": "npm ci --prefix ./Metrics",
|
||||||
"install:AdminPanel/server": "npm ci --prefix ./AdminPanel/server",
|
"install:AdminPanel/server": "npm ci --prefix ./AdminPanel/server",
|
||||||
"install:AdminPanel/client": "npm ci --prefix ./AdminPanel/client && npm --prefix ./AdminPanel/client run build",
|
"install:AdminPanel/client": "npm ci --include=dev --prefix ./AdminPanel/client && npm --prefix ./AdminPanel/client run build",
|
||||||
"3d-party-lic-json:Common": "license-report --output=json --package=./Common/package.json --config ./3d-party-lic-report/license-report-config.json > ./3d-party-lic-report/license-report.json",
|
"3d-party-lic-json:Common": "license-report --output=json --package=./Common/package.json --config ./3d-party-lic-report/license-report-config.json > ./3d-party-lic-report/license-report.json",
|
||||||
"3d-party-lic-json:DocService": "license-report --output=json --package=./DocService/package.json --config ./3d-party-lic-report/license-report-config.json > ./3d-party-lic-report/license-report.json",
|
"3d-party-lic-json:DocService": "license-report --output=json --package=./DocService/package.json --config ./3d-party-lic-report/license-report-config.json > ./3d-party-lic-report/license-report.json",
|
||||||
"3d-party-lic-json:FileConverter": "license-report --output=json --package=./FileConverter/package.json --config ./3d-party-lic-report/license-report-config.json > ./3d-party-lic-report/license-report.json",
|
"3d-party-lic-json:FileConverter": "license-report --output=json --package=./FileConverter/package.json --config ./3d-party-lic-report/license-report-config.json > ./3d-party-lic-report/license-report.json",
|
||||||
|
|||||||
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