feat: add configurable refresh interval to Clients page (closes #32)
Add a right-justified refresh interval dropdown above the client table with options for Not Updating, 30 s, 1 min, and 3 min. The chosen interval controls both React Query polling and WebSocket-triggered invalidations — selecting Not Updating fully freezes the list until page reload. Preference persists via localStorage.
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A self-hosted, full-stack network management platform for MikroTik devices. Monitor, configure, and manage your entire MikroTik infrastructure — routers, switches, and wireless access points — from a single web interface.
|
A self-hosted, full-stack network management platform for MikroTik devices. Monitor, configure, and manage your entire MikroTik infrastructure — routers, switches, and wireless access points — from a single web interface.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -150,6 +150,7 @@ Each service supports multi-device management with conflict detection:
|
|||||||
- Filter by device, type, active status, or search by MAC/IP/hostname
|
- Filter by device, type, active status, or search by MAC/IP/hostname
|
||||||
- Client detail page with connection history and vendor identification
|
- Client detail page with connection history and vendor identification
|
||||||
- Historical client count metrics
|
- Historical client count metrics
|
||||||
|
- Configurable list refresh interval (Not Updating / 30 s / 1 min / 3 min) with preference saved per browser
|
||||||
|
|
||||||
### Backups
|
### Backups
|
||||||
- Trigger RouterOS backups on demand via SSH
|
- Trigger RouterOS backups on demand via SSH
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mikrotik-manager-backend",
|
"name": "mikrotik-manager-backend",
|
||||||
"version": "0.10.6-beta",
|
"version": "0.10.7-beta",
|
||||||
"description": "Mikrotik Network Management Platform - Backend API",
|
"description": "Mikrotik Network Management Platform - Backend API",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mikrotik-manager-frontend",
|
"name": "mikrotik-manager-frontend",
|
||||||
"version": "0.10.6-beta",
|
"version": "0.10.7-beta",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo, useRef } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Search, Wifi, Network, Users, X, Pencil, Trash2, ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react';
|
import { Search, Wifi, Network, Users, X, Pencil, Trash2, ChevronUp, ChevronDown, ChevronsUpDown, RefreshCw } from 'lucide-react';
|
||||||
import { clientsApi } from '../services/api';
|
import { clientsApi } from '../services/api';
|
||||||
import type { Client } from '../types';
|
import type { Client } from '../types';
|
||||||
import { useCanWrite } from '../hooks/useCanWrite';
|
import { useCanWrite } from '../hooks/useCanWrite';
|
||||||
@@ -10,6 +10,22 @@ import { formatDistanceToNow } from 'date-fns';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
|
||||||
|
const REFRESH_OPTIONS = [
|
||||||
|
{ label: 'Not Updating', value: null },
|
||||||
|
{ label: '30 seconds', value: 30_000 },
|
||||||
|
{ label: '1 minute', value: 60_000 },
|
||||||
|
{ label: '3 minutes', value: 180_000 },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'clients-refresh-interval';
|
||||||
|
|
||||||
|
function readStoredInterval(): number | null {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (raw === 'null') return null;
|
||||||
|
const n = Number(raw);
|
||||||
|
return REFRESH_OPTIONS.some((o) => o.value === n) ? n : 30_000;
|
||||||
|
}
|
||||||
|
|
||||||
function ClientModal({
|
function ClientModal({
|
||||||
client,
|
client,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -113,8 +129,17 @@ export default function ClientsPage() {
|
|||||||
const [editingClient, setEditingClient] = useState<Client | null>(null);
|
const [editingClient, setEditingClient] = useState<Client | null>(null);
|
||||||
const [sortCol, setSortCol] = useState<string>('last_seen');
|
const [sortCol, setSortCol] = useState<string>('last_seen');
|
||||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
|
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
|
||||||
|
const [refreshInterval, setRefreshInterval] = useState<number | null>(readStoredInterval);
|
||||||
|
const refreshIntervalRef = useRef(refreshInterval);
|
||||||
|
refreshIntervalRef.current = refreshInterval;
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
|
const handleIntervalChange = (val: number | null) => {
|
||||||
|
setRefreshInterval(val);
|
||||||
|
refreshIntervalRef.current = val;
|
||||||
|
localStorage.setItem(STORAGE_KEY, String(val));
|
||||||
|
};
|
||||||
|
|
||||||
const toggleSort = (col: string) => {
|
const toggleSort = (col: string) => {
|
||||||
if (sortCol === col) setSortDir((d) => d === 'asc' ? 'desc' : 'asc');
|
if (sortCol === col) setSortDir((d) => d === 'asc' ? 'desc' : 'asc');
|
||||||
else { setSortCol(col); setSortDir('asc'); }
|
else { setSortCol(col); setSortDir('asc'); }
|
||||||
@@ -126,7 +151,7 @@ export default function ClientsPage() {
|
|||||||
clientsApi
|
clientsApi
|
||||||
.list({ search: search || undefined, active: showAll ? undefined : true, limit: PAGE_SIZE, offset: page * PAGE_SIZE })
|
.list({ search: search || undefined, active: showAll ? undefined : true, limit: PAGE_SIZE, offset: page * PAGE_SIZE })
|
||||||
.then((r) => r.data),
|
.then((r) => r.data),
|
||||||
refetchInterval: 30_000,
|
refetchInterval: refreshInterval ?? false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [purgeResult, setPurgeResult] = useState('');
|
const [purgeResult, setPurgeResult] = useState('');
|
||||||
@@ -140,7 +165,11 @@ export default function ClientsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useSocket({
|
useSocket({
|
||||||
'clients:updated': () => queryClient.invalidateQueries({ queryKey: ['clients'] }),
|
'clients:updated': () => {
|
||||||
|
if (refreshIntervalRef.current !== null) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['clients'] });
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const rawClients = data?.clients ?? [];
|
const rawClients = data?.clients ?? [];
|
||||||
@@ -227,6 +256,23 @@ export default function ClientsPage() {
|
|||||||
{purgeResult && (
|
{purgeResult && (
|
||||||
<span className="text-xs text-green-600 dark:text-green-400">{purgeResult}</span>
|
<span className="text-xs text-green-600 dark:text-green-400">{purgeResult}</span>
|
||||||
)}
|
)}
|
||||||
|
<div className="flex items-center gap-1.5 ml-auto">
|
||||||
|
<RefreshCw className="w-3.5 h-3.5 text-gray-400 dark:text-slate-500" />
|
||||||
|
<select
|
||||||
|
value={String(refreshInterval)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value;
|
||||||
|
handleIntervalChange(raw === 'null' ? null : Number(raw));
|
||||||
|
}}
|
||||||
|
className="text-sm rounded-lg border border-gray-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-gray-700 dark:text-slate-200 px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
{REFRESH_OPTIONS.map((o) => (
|
||||||
|
<option key={String(o.value)} value={String(o.value)}>
|
||||||
|
{o.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const APP_VERSION = 'v0.10.6 Beta';
|
export const APP_VERSION = 'v0.10.7 Beta';
|
||||||
|
|||||||
Reference in New Issue
Block a user