mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-08 19:16:37 +08:00
usagestats v0
This commit is contained in:
parent
f871e2b8bb
commit
595cf6bf46
76
app/components/usage-stats/UsageStats.module.scss
Normal file
76
app/components/usage-stats/UsageStats.module.scss
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// UsageStats.module.scss
|
||||||
|
|
||||||
|
.usageStatsContainer {
|
||||||
|
position: fixed; // Fixed position to overlay on top of the content
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5); // Semi-transparent background to obscure the content
|
||||||
|
z-index: 1000; // High z-index to ensure it's on top of other elements
|
||||||
|
}
|
||||||
|
|
||||||
|
.usageStatsModal {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 80%;
|
||||||
|
max-width: 600px; // Maximum width of the modal
|
||||||
|
z-index: 1001; // Ensure the modal is above the semi-transparent background
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... Add more styles for headings, buttons, tables, etc.
|
||||||
|
.closeButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dropdown {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... Add more styles as needed
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.usageStatsModal {
|
||||||
|
width: 95%;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
79
app/components/usage-stats/UsageStats.tsx
Normal file
79
app/components/usage-stats/UsageStats.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// UsageStats.tsx
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { getAvailableDateKeys, getSignInCountForPeriod, getDetailsByUser } from './app/utils/cloud/redisRestClient';
|
||||||
|
import styles from './UsageStats.module.scss'; // Assume you have a separate SCSS module for UsageStats
|
||||||
|
|
||||||
|
const UsageStats: React.FC<{ onClose: () => void }> = ({ onClose }) => {
|
||||||
|
const [dateKeys, setDateKeys] = useState<string[]>([]);
|
||||||
|
const [selectedDateKey, setSelectedDateKey] = useState<string>('');
|
||||||
|
const [signInCount, setSignInCount] = useState<number>(0);
|
||||||
|
const [userDetails, setUserDetails] = useState<Record<string, number>>({});
|
||||||
|
const [showDrillDown, setShowDrillDown] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
const availableDateKeys = await getAvailableDateKeys();
|
||||||
|
setDateKeys(availableDateKeys);
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDateChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const dateKey = event.target.value;
|
||||||
|
setSelectedDateKey(dateKey);
|
||||||
|
const count = await getSignInCountForPeriod(dateKey);
|
||||||
|
setSignInCount(count);
|
||||||
|
setShowDrillDown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrillDown = async () => {
|
||||||
|
const details = await getDetailsByUser(selectedDateKey);
|
||||||
|
setUserDetails(details);
|
||||||
|
setShowDrillDown(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.usageStatsContainer}>
|
||||||
|
<div className={styles.usageStatsModal}>
|
||||||
|
<h1>Usage Stats</h1>
|
||||||
|
<select value={selectedDateKey} onChange={handleDateChange}>
|
||||||
|
{dateKeys.map(dateKey => (
|
||||||
|
<option key={dateKey} value={dateKey}>
|
||||||
|
{dateKey}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<p>Number of events: {signInCount}</p>
|
||||||
|
<button onClick={handleDrillDown}>Drill-down</button>
|
||||||
|
{showDrillDown && (
|
||||||
|
|
||||||
|
|
||||||
|
{/* ... other UI elements ... */}
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Count</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(userDetails).map(([email, count]) => (
|
||||||
|
<tr key={email}>
|
||||||
|
<td>{email}</td>
|
||||||
|
<td>{count}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
<button className={styles.closeButton} onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UsageStats;
|
@ -60,3 +60,35 @@ export const incrementTokenCounts = async (
|
|||||||
console.error('Failed to increment token counts in Redis via Upstash', error);
|
console.error('Failed to increment token counts in Redis via Upstash', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const getAvailableDateKeys = async (): Promise<string[]> => {
|
||||||
|
try {
|
||||||
|
const keys = await redis.keys('signin_count:*');
|
||||||
|
return keys.map(key => key.split(':')[1]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get keys from Redis', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSignInCountForPeriod = async (dateKey: string): Promise<number> => {
|
||||||
|
try {
|
||||||
|
const counts = await redis.hgetall(`signin_count:${dateKey}`);
|
||||||
|
return Object.values(counts).reduce((total, count) => total + parseInt(count, 10), 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to get sign-in count for period ${dateKey}`, error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDetailsByUser = async (dateKey: string): Promise<Record<string, number>> => {
|
||||||
|
try {
|
||||||
|
const counts = await redis.hgetall(`signin_count:${dateKey}`);
|
||||||
|
return counts;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to get details by user for period ${dateKey}`, error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user