Skip to content

Commit ed8cb48

Browse files
committed
Enhance Dashboard and Logs components: add service status link in Dashboard, implement time range filter in Logs with options for various intervals, and update default time range to 5 minutes in NetworkView. Improve filtering logic and UI for better user experience.
1 parent ed4588b commit ed8cb48

File tree

3 files changed

+100
-61
lines changed

3 files changed

+100
-61
lines changed

frontend/src/pages/Dashboard.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,19 @@ export default function Dashboard() {
7979
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-2">
8080
{connectionStatus}
8181
</p>
82-
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1">
83-
API Connection
84-
</p>
82+
<div className="flex items-center justify-between mt-1">
83+
<p className="text-sm text-gray-600 dark:text-gray-300">
84+
API Connection
85+
</p>
86+
<a
87+
href="https://status.tailscale.com/"
88+
target="_blank"
89+
rel="noopener noreferrer"
90+
className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 transition-colors hover:underline"
91+
>
92+
Service Status ↗
93+
</a>
94+
</div>
8595
</div>
8696

8797
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm p-6">

frontend/src/pages/Logs.tsx

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export default function Logs() {
5959
const [useCustomTimeRange, setUseCustomTimeRange] = useState(false)
6060
const [startDate, setStartDate] = useState('')
6161
const [endDate, setEndDate] = useState('')
62+
const [timeRangeFilter, setTimeRangeFilter] = useState<string>('5m')
6263

6364
// Fetch Tailscale network logs
6465
const networkLogsApiUrl = useMemo(() => {
@@ -68,16 +69,44 @@ export default function Logs() {
6869
if (useCustomTimeRange && startDate && endDate) {
6970
params.append('start', new Date(startDate).toISOString())
7071
params.append('end', new Date(endDate).toISOString())
71-
} else {
72-
// Default to last 5 minutes
72+
} else if (timeRangeFilter !== 'all') {
73+
// Convert time range to timestamp
7374
const now = new Date()
74-
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000)
75-
params.append('start', fiveMinutesAgo.toISOString())
75+
let since: Date
76+
switch (timeRangeFilter) {
77+
case '5m':
78+
since = new Date(now.getTime() - 5 * 60 * 1000)
79+
break
80+
case '15m':
81+
since = new Date(now.getTime() - 15 * 60 * 1000)
82+
break
83+
case '30m':
84+
since = new Date(now.getTime() - 30 * 60 * 1000)
85+
break
86+
case '1h':
87+
since = new Date(now.getTime() - 60 * 60 * 1000)
88+
break
89+
case '6h':
90+
since = new Date(now.getTime() - 6 * 60 * 60 * 1000)
91+
break
92+
case '24h':
93+
since = new Date(now.getTime() - 24 * 60 * 60 * 1000)
94+
break
95+
case '7d':
96+
since = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
97+
break
98+
case '30d':
99+
since = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
100+
break
101+
default:
102+
since = new Date(now.getTime() - 5 * 60 * 1000) // Default to last 5m
103+
}
104+
params.append('start', since.toISOString())
76105
params.append('end', now.toISOString())
77106
}
78107

79108
return `${baseUrl}?${params.toString()}`
80-
}, [useCustomTimeRange, startDate, endDate])
109+
}, [timeRangeFilter, useCustomTimeRange, startDate, endDate])
81110

82111
const { data: networkLogsData, mutate: refetchNetworkLogs } = useSWR(networkLogsApiUrl, fetcher, {
83112
errorRetryCount: 2,
@@ -169,7 +198,7 @@ export default function Logs() {
169198
// Reset to first page when filters change
170199
useEffect(() => {
171200
setCurrentPage(1)
172-
}, [searchQuery, protocolFilter, trafficTypeFilter, useCustomTimeRange, startDate, endDate])
201+
}, [searchQuery, protocolFilter, trafficTypeFilter, timeRangeFilter, useCustomTimeRange, startDate, endDate])
173202

174203
// Calculate pagination values
175204
const totalPages = Math.ceil(filteredEntries.length / itemsPerPage)
@@ -220,7 +249,7 @@ export default function Logs() {
220249
<span></span>
221250
<span>{useCustomTimeRange && startDate && endDate ?
222251
`${new Date(startDate).toLocaleDateString()} - ${new Date(endDate).toLocaleDateString()}` :
223-
'Last 5 minutes'}</span>
252+
timeRangeFilter}</span>
224253
</div>
225254
<div className="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
226255
<Calendar className="w-4 h-4" />
@@ -268,7 +297,7 @@ export default function Logs() {
268297
</label>
269298
</div>
270299

271-
{useCustomTimeRange && (
300+
{useCustomTimeRange ? (
272301
<div className="space-y-3">
273302
<div>
274303
<label className="block text-xs text-gray-500 dark:text-gray-400 mb-1">Start Date & Time</label>
@@ -289,6 +318,23 @@ export default function Logs() {
289318
/>
290319
</div>
291320
</div>
321+
) : (
322+
<div className="flex space-x-2">
323+
<select
324+
value={timeRangeFilter}
325+
onChange={(e) => setTimeRangeFilter(e.target.value)}
326+
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
327+
>
328+
<option value="5m">Last 5 Minutes</option>
329+
<option value="15m">Last 15 Minutes</option>
330+
<option value="30m">Last 30 Minutes</option>
331+
<option value="1h">Last Hour</option>
332+
<option value="6h">Last 6 Hours</option>
333+
<option value="24h">Last 24 Hours</option>
334+
<option value="7d">Last 7 Days</option>
335+
<option value="30d">Last 30 Days</option>
336+
</select>
337+
</div>
292338
)}
293339

294340
<button
@@ -372,7 +418,7 @@ export default function Logs() {
372418
<div>Log Entries: {networkLogs.length.toLocaleString()}</div>
373419
<div>Time Range: {useCustomTimeRange && startDate && endDate ?
374420
`${new Date(startDate).toLocaleDateString()} - ${new Date(endDate).toLocaleDateString()}` :
375-
'Last 5 minutes'}</div>
421+
timeRangeFilter}</div>
376422
</div>
377423
</div>
378424
</div>

frontend/src/pages/NetworkView.tsx

Lines changed: 32 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ const getProtocolName = (proto: number): string => {
7777

7878
// Helper function to categorize IP addresses
7979
const categorizeIP = (ip: string): string[] => {
80+
// DERP servers
81+
if (ip === '127.3.3.40') return ['derp']
82+
8083
// IPv4 Tailscale addresses
8184
if (ip.startsWith('100.')) return ['tailscale']
8285

@@ -128,7 +131,7 @@ const NetworkView: React.FC = () => {
128131
const [trafficTypeFilters, setTrafficTypeFilters] = useState<Set<string>>(new Set())
129132
const [ipCategoryFilters, setIpCategoryFilters] = useState<Set<string>>(new Set())
130133
const [ipVersionFilter, setIpVersionFilter] = useState<string>('all') // IPv4/IPv6 filter
131-
const [timeRangeFilter, setTimeRangeFilter] = useState<string>('1h')
134+
const [timeRangeFilter, setTimeRangeFilter] = useState<string>('5m')
132135
const [minBandwidth, setMinBandwidth] = useState<number>(0)
133136
const [maxBandwidth, setMaxBandwidth] = useState<number>(1000000000) // 1GB
134137
const [nodeCountFilter, setNodeCountFilter] = useState<number>(0) // Minimum connections
@@ -180,7 +183,7 @@ const NetworkView: React.FC = () => {
180183
since = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
181184
break
182185
default:
183-
since = new Date(now.getTime() - 60 * 60 * 1000) // Default to last 1h
186+
since = new Date(now.getTime() - 5 * 60 * 1000) // Default to last 5m
184187
}
185188
params.append('start', since.toISOString())
186189
params.append('end', now.toISOString())
@@ -197,10 +200,10 @@ const NetworkView: React.FC = () => {
197200

198201
const networkLogs = (Array.isArray(networkLogsData) && networkLogsData.length > 0 && 'logged' in networkLogsData[0]) ? networkLogsData : []
199202

200-
// Set default date range to show most recent data (last 1 hour)
203+
// Set default date range to show most recent data (last 5 minutes)
201204
useEffect(() => {
202205
const now = new Date()
203-
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000)
206+
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000)
204207

205208
// Format for datetime-local input (YYYY-MM-DDTHH:mm)
206209
const formatForInput = (date: Date) => {
@@ -212,7 +215,7 @@ const NetworkView: React.FC = () => {
212215
return `${year}-${month}-${day}T${hours}:${minutes}`
213216
}
214217

215-
setStartDate(formatForInput(oneHourAgo))
218+
setStartDate(formatForInput(fiveMinutesAgo))
216219
setEndDate(formatForInput(now))
217220
setLoading(false)
218221
}, [])
@@ -457,46 +460,20 @@ const NetworkView: React.FC = () => {
457460

458461
svg.call(zoom)
459462

460-
// Group nodes by category for better clustering
461-
const nodesByCategory = filteredData.nodes.reduce((acc, node) => {
462-
const primaryTag = node.tags.find(tag => !tag.includes('+')) || 'unknown'
463-
if (!acc[primaryTag]) acc[primaryTag] = []
464-
acc[primaryTag].push(node)
465-
return acc
466-
}, {} as Record<string, typeof filteredData.nodes>)
467-
468-
// Calculate category centers in a grid pattern
469-
const categories = Object.keys(nodesByCategory)
470-
const cols = Math.ceil(Math.sqrt(categories.length))
471-
const categoryPositions = categories.reduce((acc, category, index) => {
472-
const row = Math.floor(index / cols)
473-
const col = index % cols
474-
const offsetX = (width / cols) * col + (width / cols) / 2
475-
const offsetY = (height / Math.ceil(categories.length / cols)) * row + (height / Math.ceil(categories.length / cols)) / 2
476-
acc[category] = { x: offsetX, y: offsetY }
477-
return acc
478-
}, {} as Record<string, { x: number; y: number }>)
479-
480-
// Create force simulation with better spacing and clustering
463+
464+
465+
// Create force simulation with no gravity - pure network topology layout
481466
const simulation = d3.forceSimulation(filteredData.nodes)
482-
.force('link', d3.forceLink(filteredData.links).id((d: any) => d.id).distance(250).strength(0.4))
483-
.force('charge', d3.forceManyBody().strength(-800).distanceMin(100).distanceMax(1000))
484-
.force('center', d3.forceCenter(width / 2, height / 2).strength(0.02))
485-
.force('collision', d3.forceCollide().radius((d: any) => {
486-
const maxTextLength = Math.max(d.displayName.length, d.ip.length, 12)
467+
.force('link', d3.forceLink(filteredData.links).id((d: d3.SimulationNodeDatum) => (d as NetworkNode).id).distance(200).strength(0.5))
468+
.force('charge', d3.forceManyBody().strength(-800).distanceMin(120).distanceMax(600))
469+
.force('collision', d3.forceCollide().radius((d: d3.SimulationNodeDatum) => {
470+
const node = d as NetworkNode
471+
const maxTextLength = Math.max(node.displayName.length, node.ip.length, 12)
487472
const nodeWidth = Math.max(120, Math.min(maxTextLength * 8 + 20, 200))
488-
return nodeWidth / 2 + 25 // More padding for better separation
473+
return nodeWidth / 2 + 35 // Good spacing for fan-out
489474
}).strength(1.0).iterations(3))
490-
.force('categoryX', d3.forceX().x((d: any) => {
491-
const primaryTag = d.tags.find((tag: string) => !tag.includes('+')) || 'unknown'
492-
return categoryPositions[primaryTag]?.x || width / 2
493-
}).strength(0.1))
494-
.force('categoryY', d3.forceY().y((d: any) => {
495-
const primaryTag = d.tags.find((tag: string) => !tag.includes('+')) || 'unknown'
496-
return categoryPositions[primaryTag]?.y || height / 2
497-
}).strength(0.1))
498-
.alphaDecay(0.003)
499-
.velocityDecay(0.7)
475+
.alphaDecay(0.01) // Quick settling
476+
.velocityDecay(0.85) // Moderate friction for natural movement
500477

501478
// Create links
502479
const link = g.append('g')
@@ -601,12 +578,14 @@ const NetworkView: React.FC = () => {
601578
.attr('x', (d: NetworkNode) => -getBoxDimensions(d).width / 2)
602579
.attr('y', (d: NetworkNode) => -getBoxDimensions(d).height / 2)
603580
.attr('fill', (d: NetworkNode) => {
581+
if (d.tags.includes('derp')) return '#fecaca'
604582
if (d.tags.includes('tailscale')) return '#dbeafe'
605583
if (d.tags.includes('private')) return '#dcfce7'
606584
if (d.tags.includes('ipv6')) return '#e9d5ff'
607585
return '#fef3c7'
608586
})
609587
.attr('stroke', (d: NetworkNode) => {
588+
if (d.tags.includes('derp')) return '#dc2626'
610589
if (d.tags.includes('tailscale')) return '#3b82f6'
611590
if (d.tags.includes('private')) return '#10b981'
612591
if (d.tags.includes('ipv6')) return '#8b5cf6'
@@ -667,20 +646,20 @@ const NetworkView: React.FC = () => {
667646
connectedNodeIds.add(d.id)
668647

669648
// Hide/show nodes
670-
node.style('opacity', (n: any) => {
649+
node.style('opacity', (n: NetworkNode) => {
671650
return connectedNodeIds.has(n.id) ? 1 : 0.1
672651
})
673652

674653
// Hide/show links
675-
link.style('opacity', (l: any) => {
654+
link.style('opacity', (l: NetworkLink) => {
676655
const sourceId = typeof l.source === 'string' ? l.source : l.source.id
677656
const targetId = typeof l.target === 'string' ? l.target : l.target.id
678657
return sourceId === d.id || targetId === d.id ? 0.9 : 0.05
679658
})
680659

681660
// Highlight selected node
682661
node.selectAll('rect')
683-
.attr('stroke-width', (n: any) => n.id === d.id ? 4 : 2)
662+
.attr('stroke-width', (n: unknown) => (n as NetworkNode).id === d.id ? 4 : 2)
684663
})
685664

686665
// Handle link clicks with improved hiding
@@ -767,13 +746,13 @@ const NetworkView: React.FC = () => {
767746
setSearchQuery('')
768747

769748
// Reset time range to default
770-
setTimeRangeFilter('1h')
749+
setTimeRangeFilter('5m')
771750
setUseCustomTimeRange(false)
772751

773752
// Reset all filter sets to include all options
774753
if (uniqueProtocols.length > 0) setProtocolFilters(new Set(uniqueProtocols))
775754
if (uniqueTrafficTypes.length > 0) setTrafficTypeFilters(new Set(uniqueTrafficTypes))
776-
if (uniqueIpCategories.length > 0) setIpCategoryFilters(new Set(uniqueIpCategories.filter(cat => cat !== 'ipv6')))
755+
if (uniqueIpCategories.length > 0) setIpCategoryFilters(new Set(uniqueIpCategories.filter(cat => cat !== 'ipv6' && cat !== 'derp'))) // Keep derp hidden on reset
777756

778757
// Reset other filters
779758
setIpVersionFilter('all')
@@ -849,7 +828,7 @@ const NetworkView: React.FC = () => {
849828
// Get unique values for filters with common defaults
850829
const baseProtocols = ['TCP', 'UDP', 'ICMP', 'Proto-255', 'Proto-0'] // Added Proto-0 to base protocols
851830
const baseTrafficTypes = ['virtual', 'subnet', 'physical'] // All possible traffic types
852-
const baseIpCategories = ['tailscale', 'private', 'public'] // Common IP categories
831+
const baseIpCategories = ['tailscale', 'private', 'public', 'derp'] // Common IP categories
853832

854833
const dataProtocols = Array.from(new Set(links.map(l => l.protocol)))
855834
const dataTrafficTypes = Array.from(new Set(links.map(l => l.trafficType)))
@@ -866,7 +845,7 @@ const NetworkView: React.FC = () => {
866845
if (!filtersInitialized && uniqueProtocols.length > 0 && uniqueTrafficTypes.length > 0 && uniqueIpCategories.length > 0) {
867846
setProtocolFilters(new Set(uniqueProtocols))
868847
setTrafficTypeFilters(new Set(uniqueTrafficTypes))
869-
setIpCategoryFilters(new Set(uniqueIpCategories.filter(cat => cat !== 'ipv6')))
848+
setIpCategoryFilters(new Set(uniqueIpCategories.filter(cat => cat !== 'ipv6' && cat !== 'derp'))) // Hide derp by default
870849
setFiltersInitialized(true)
871850
}
872851
}, [uniqueProtocols, uniqueTrafficTypes, uniqueIpCategories, filtersInitialized])
@@ -1412,6 +1391,10 @@ const NetworkView: React.FC = () => {
14121391
<div className="w-3 h-3 border-2 border-yellow-500 bg-yellow-100 dark:bg-yellow-900 mr-2"></div>
14131392
<span>Public Internet</span>
14141393
</div>
1394+
<div className="flex items-center">
1395+
<div className="w-3 h-3 border-2 border-red-600 bg-red-100 dark:bg-red-900 mr-2"></div>
1396+
<span>DERP Servers</span>
1397+
</div>
14151398
</div>
14161399

14171400
<div className="mt-4">

0 commit comments

Comments
 (0)