mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Settings display demo and login fix (#4884)
# Description of Changes <img width="1569" height="980" alt="image" src="https://github.com/user-attachments/assets/dca1c227-ed84-4393-97a1-e3ce6eb1620b" /> <img width="1596" height="935" alt="image" src="https://github.com/user-attachments/assets/2003e1be-034a-4cbb-869e-6d5d912ab61d" /> <img width="1543" height="997" alt="image" src="https://github.com/user-attachments/assets/fe0c4f4b-eeee-4db4-a041-e554f350255a" /> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
+261
@@ -0,0 +1,261 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Stack,
|
||||
Group,
|
||||
Text,
|
||||
Button,
|
||||
SegmentedControl,
|
||||
Loader,
|
||||
Alert,
|
||||
Card,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import usageAnalyticsService, { EndpointStatisticsResponse } from '@app/services/usageAnalyticsService';
|
||||
import UsageAnalyticsChart from '@app/components/shared/config/configSections/usage/UsageAnalyticsChart';
|
||||
import UsageAnalyticsTable from '@app/components/shared/config/configSections/usage/UsageAnalyticsTable';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
|
||||
const AdminUsageSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { loginEnabled, validateLoginEnabled } = useLoginRequired();
|
||||
const [data, setData] = useState<EndpointStatisticsResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [displayMode, setDisplayMode] = useState<'top10' | 'top20' | 'all'>('top10');
|
||||
const [dataType, setDataType] = useState<'all' | 'api' | 'ui'>('all');
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const limit = displayMode === 'all' ? undefined : displayMode === 'top10' ? 10 : 20;
|
||||
const response = await usageAnalyticsService.getEndpointStatistics(limit, dataType);
|
||||
|
||||
setData(response);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load usage statistics');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (loginEnabled) {
|
||||
fetchData();
|
||||
} else {
|
||||
// Provide example usage analytics data when login is disabled
|
||||
const totalVisits = 15847;
|
||||
const allEndpoints = [
|
||||
{ endpoint: 'merge-pdfs', visits: 3245, percentage: (3245 / totalVisits) * 100 },
|
||||
{ endpoint: 'compress-pdf', visits: 2891, percentage: (2891 / totalVisits) * 100 },
|
||||
{ endpoint: 'pdf-to-img', visits: 2156, percentage: (2156 / totalVisits) * 100 },
|
||||
{ endpoint: 'split-pdf', visits: 1834, percentage: (1834 / totalVisits) * 100 },
|
||||
{ endpoint: 'rotate-pdf', visits: 1523, percentage: (1523 / totalVisits) * 100 },
|
||||
{ endpoint: 'ocr-pdf', visits: 1287, percentage: (1287 / totalVisits) * 100 },
|
||||
{ endpoint: 'add-watermark', visits: 945, percentage: (945 / totalVisits) * 100 },
|
||||
{ endpoint: 'extract-images', visits: 782, percentage: (782 / totalVisits) * 100 },
|
||||
{ endpoint: 'add-password', visits: 621, percentage: (621 / totalVisits) * 100 },
|
||||
{ endpoint: 'html-to-pdf', visits: 563, percentage: (563 / totalVisits) * 100 },
|
||||
{ endpoint: 'remove-password', visits: 487, percentage: (487 / totalVisits) * 100 },
|
||||
{ endpoint: 'pdf-to-pdfa', visits: 423, percentage: (423 / totalVisits) * 100 },
|
||||
{ endpoint: 'extract-pdf-metadata', visits: 356, percentage: (356 / totalVisits) * 100 },
|
||||
{ endpoint: 'add-page-numbers', visits: 298, percentage: (298 / totalVisits) * 100 },
|
||||
{ endpoint: 'crop', visits: 245, percentage: (245 / totalVisits) * 100 },
|
||||
{ endpoint: 'flatten', visits: 187, percentage: (187 / totalVisits) * 100 },
|
||||
{ endpoint: 'sanitize-pdf', visits: 134, percentage: (134 / totalVisits) * 100 },
|
||||
{ endpoint: 'auto-split-pdf', visits: 98, percentage: (98 / totalVisits) * 100 },
|
||||
{ endpoint: 'scale-pages', visits: 76, percentage: (76 / totalVisits) * 100 },
|
||||
{ endpoint: 'compare-pdfs', visits: 42, percentage: (42 / totalVisits) * 100 },
|
||||
];
|
||||
|
||||
// Filter based on display mode
|
||||
let filteredEndpoints = allEndpoints;
|
||||
if (displayMode === 'top10') {
|
||||
filteredEndpoints = allEndpoints.slice(0, 10);
|
||||
} else if (displayMode === 'top20') {
|
||||
filteredEndpoints = allEndpoints.slice(0, 20);
|
||||
}
|
||||
|
||||
setData({
|
||||
totalVisits: totalVisits,
|
||||
totalEndpoints: filteredEndpoints.length,
|
||||
endpoints: filteredEndpoints,
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
}, [displayMode, dataType, loginEnabled]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (!validateLoginEnabled()) {
|
||||
return;
|
||||
}
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const getDisplayModeLabel = () => {
|
||||
switch (displayMode) {
|
||||
case 'top10':
|
||||
return t('usage.showing.top10', 'Top 10');
|
||||
case 'top20':
|
||||
return t('usage.showing.top20', 'Top 20');
|
||||
case 'all':
|
||||
return t('usage.showing.all', 'All');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// Override loading state when login is disabled
|
||||
const actualLoading = loginEnabled ? loading : false;
|
||||
|
||||
// Early returns for loading/error states
|
||||
if (actualLoading) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '2rem' }}>
|
||||
<Loader size="lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert color="red" title={t('usage.error', 'Error loading usage statistics')}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Alert color="yellow" title={t('usage.noData', 'No data available')}>
|
||||
{t('usage.noDataMessage', 'No usage statistics are currently available.')}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const chartData = data?.endpoints?.map((e) => ({ label: e.endpoint, value: e.visits })) || [];
|
||||
|
||||
const displayedVisits = data?.endpoints?.reduce((sum, e) => sum + e.visits, 0) || 0;
|
||||
|
||||
const displayedPercentage = (data?.totalVisits || 0) > 0
|
||||
? ((displayedVisits / (data?.totalVisits || 1)) * 100).toFixed(1)
|
||||
: '0';
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<LoginRequiredBanner show={!loginEnabled} />
|
||||
|
||||
{/* Controls */}
|
||||
<Card padding="lg" radius="md" withBorder>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" wrap="wrap">
|
||||
<Group>
|
||||
<SegmentedControl
|
||||
value={displayMode}
|
||||
onChange={(value) => setDisplayMode(value as 'top10' | 'top20' | 'all')}
|
||||
disabled={!loginEnabled}
|
||||
data={[
|
||||
{
|
||||
value: 'top10',
|
||||
label: t('usage.controls.top10', 'Top 10'),
|
||||
},
|
||||
{
|
||||
value: 'top20',
|
||||
label: t('usage.controls.top20', 'Top 20'),
|
||||
},
|
||||
{
|
||||
value: 'all',
|
||||
label: t('usage.controls.all', 'All'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
leftSection={<LocalIcon icon="refresh" width="1rem" height="1rem" />}
|
||||
onClick={handleRefresh}
|
||||
loading={loading}
|
||||
disabled={!loginEnabled}
|
||||
>
|
||||
{t('usage.controls.refresh', 'Refresh')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
<Text size="sm" fw={500}>
|
||||
{t('usage.controls.dataTypeLabel', 'Data Type:')}
|
||||
</Text>
|
||||
<SegmentedControl
|
||||
value={dataType}
|
||||
onChange={(value) => setDataType(value as 'all' | 'api' | 'ui')}
|
||||
disabled={!loginEnabled}
|
||||
data={[
|
||||
{
|
||||
value: 'all',
|
||||
label: t('usage.controls.dataType.all', 'All'),
|
||||
},
|
||||
{
|
||||
value: 'api',
|
||||
label: t('usage.controls.dataType.api', 'API'),
|
||||
},
|
||||
{
|
||||
value: 'ui',
|
||||
label: t('usage.controls.dataType.ui', 'UI'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{/* Statistics Summary */}
|
||||
<Group gap="xl" style={{ flexWrap: 'wrap' }}>
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('usage.stats.totalEndpoints', 'Total Endpoints')}
|
||||
</Text>
|
||||
<Text size="lg" fw={600}>
|
||||
{data.totalEndpoints}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('usage.stats.totalVisits', 'Total Visits')}
|
||||
</Text>
|
||||
<Text size="lg" fw={600}>
|
||||
{data.totalVisits.toLocaleString()}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('usage.stats.showing', 'Showing')}
|
||||
</Text>
|
||||
<Text size="lg" fw={600}>
|
||||
{getDisplayModeLabel()}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('usage.stats.selectedVisits', 'Selected Visits')}
|
||||
</Text>
|
||||
<Text size="lg" fw={600}>
|
||||
{displayedVisits.toLocaleString()} ({displayedPercentage}%)
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Chart and Table */}
|
||||
<UsageAnalyticsChart data={chartData} />
|
||||
<UsageAnalyticsTable data={data.endpoints} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminUsageSection;
|
||||
Reference in New Issue
Block a user