1
1
import classNames from "classnames" ;
2
- import { FC , useState , useRef , useEffect , useCallback } from "react" ;
2
+ import { FC , useState , useRef , useEffect , useCallback , useMemo } from "react" ;
3
3
import { createPortal } from "react-dom" ;
4
4
import { Icons } from "./icons" ;
5
5
import { ClassNames } from "./classes" ;
@@ -11,31 +11,95 @@ interface ProfileInfoTooltipProps {
11
11
className ?: string ;
12
12
}
13
13
14
- function getPortFromAdvanced ( profile : LocalLoginProfile ) : string {
14
+ // Profile ID validation: only allow alphanumeric, hyphens, underscores, max 64 chars
15
+ function isValidProfileId ( profileId : string ) : boolean {
16
+ return typeof profileId === 'string' &&
17
+ profileId . length > 0 &&
18
+ profileId . length <= 64 &&
19
+ / ^ [ a - z A - Z 0 - 9 _ - ] + $ / . test ( profileId ) ;
20
+ }
21
+
22
+ // IPv6-safe hostname/port extraction
23
+ function extractPortFromHostname ( hostname : string ) : string {
24
+ if ( ! hostname || typeof hostname !== 'string' ) {
25
+ return 'Default' ;
26
+ }
27
+
28
+ // Handle IPv6 addresses by checking for square brackets
29
+ if ( hostname . includes ( '[' ) ) {
30
+ const match = hostname . match ( / \] : ( \d + ) $ / ) ;
31
+ return match ? match [ 1 ] : 'Default' ;
32
+ }
33
+
34
+ const parts = hostname . split ( ':' ) ;
35
+ if ( parts . length > 1 ) {
36
+ const port = parts [ parts . length - 1 ] ;
37
+ // Check if the last part is numeric (a port)
38
+ if ( / ^ \d + $ / . test ( port ) ) {
39
+ return port ;
40
+ }
41
+ }
42
+ return 'Default' ;
43
+ }
44
+
45
+ function getPortFromAdvanced ( profile : LocalLoginProfile ) : string | null {
15
46
const dbType = profile . Type ;
16
- const defaultPort = databaseTypeDropdownItems . find ( item => item . id === dbType ) ! . extra ! . Port ;
47
+ const defaultPortItem = databaseTypeDropdownItems . find ( item => item . id === dbType ) ;
48
+
49
+ if ( ! defaultPortItem ?. extra ?. Port ) {
50
+ return null ; // No default port found, hide this info
51
+ }
52
+
53
+ const defaultPort = defaultPortItem . extra . Port ;
17
54
18
55
if ( profile . Advanced ) {
19
56
const portObj = profile . Advanced . find ( item => item . Key === 'Port' ) ;
20
57
return portObj ?. Value || defaultPort ;
21
58
}
22
59
60
+ // Check if hostname contains port info
61
+ if ( profile . Host ) {
62
+ const extractedPort = extractPortFromHostname ( profile . Host ) ;
63
+ if ( extractedPort !== 'Default' ) {
64
+ return extractedPort ;
65
+ }
66
+ }
67
+
23
68
return defaultPort ;
24
69
}
25
70
26
- function getLastAccessedTime ( profileId : string ) : string {
71
+ function getLastAccessedTime ( profileId : string ) : string | null {
72
+ if ( ! isValidProfileId ( profileId ) ) {
73
+ return null ; // Invalid profile ID, hide this info
74
+ }
75
+
27
76
try {
28
77
const lastAccessed = localStorage . getItem ( `whodb_profile_last_accessed_${ profileId } ` ) ;
29
78
if ( lastAccessed ) {
30
79
const date = new Date ( lastAccessed ) ;
80
+ if ( isNaN ( date . getTime ( ) ) ) {
81
+ return null ; // Invalid date, hide this info
82
+ }
31
83
const timeZone = Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone ;
32
84
const formattedTimeZone = timeZone . replace ( / _ / g, ' ' ) . split ( '/' ) . join ( ' / ' ) ;
33
85
return `${ date . toLocaleDateString ( ) } ${ date . toLocaleTimeString ( [ ] , { hour : '2-digit' , minute : '2-digit' } ) } (${ formattedTimeZone } )` ;
34
86
}
35
87
} catch ( error ) {
36
- console . warn ( 'Failed to get last accessed time:' , error ) ;
88
+ // Silently fail - return null to hide this info
37
89
}
38
- return 'Never' ;
90
+ return null ;
91
+ }
92
+
93
+ // Portal container reuse - create once and reuse
94
+ let tooltipPortalContainer : HTMLDivElement | null = null ;
95
+
96
+ function getTooltipPortalContainer ( ) : HTMLDivElement {
97
+ if ( ! tooltipPortalContainer ) {
98
+ tooltipPortalContainer = document . createElement ( 'div' ) ;
99
+ tooltipPortalContainer . id = 'whodb-tooltip-portal' ;
100
+ document . body . appendChild ( tooltipPortalContainer ) ;
101
+ }
102
+ return tooltipPortalContainer ;
39
103
}
40
104
41
105
export const ProfileInfoTooltip : FC < ProfileInfoTooltipProps > = ( { profile, className } ) => {
@@ -46,6 +110,12 @@ export const ProfileInfoTooltip: FC<ProfileInfoTooltipProps> = ({ profile, class
46
110
const port = getPortFromAdvanced ( profile ) ;
47
111
const lastAccessed = getLastAccessedTime ( profile . Id ) ;
48
112
113
+ // If no information is available, don't render the component
114
+ const hasInfo = port !== null || lastAccessed !== null ;
115
+ if ( ! hasInfo ) {
116
+ return null ;
117
+ }
118
+
49
119
// Show tooltip to the right of the icon
50
120
const showTooltip = useCallback ( ( ) => {
51
121
if ( btnRef . current ) {
@@ -63,30 +133,35 @@ export const ProfileInfoTooltip: FC<ProfileInfoTooltipProps> = ({ profile, class
63
133
setIsVisible ( false ) ;
64
134
} , [ ] ) ;
65
135
66
- // Click-away logic
67
- useEffect ( ( ) => {
68
- if ( ! isVisible ) return ;
69
- function handleClick ( event : MouseEvent ) {
70
- if (
71
- btnRef . current &&
72
- ! btnRef . current . contains ( event . target as Node )
73
- ) {
74
- setIsVisible ( false ) ;
75
- }
136
+ // Memoized event handlers to prevent recreation
137
+ const handleClickAway = useCallback ( ( event : MouseEvent ) => {
138
+ if (
139
+ btnRef . current &&
140
+ ! btnRef . current . contains ( event . target as Node )
141
+ ) {
142
+ setIsVisible ( false ) ;
76
143
}
77
- document . addEventListener ( "mousedown" , handleClick ) ;
78
- return ( ) => document . removeEventListener ( "mousedown" , handleClick ) ;
79
- } , [ isVisible ] ) ;
144
+ } , [ ] ) ;
145
+
146
+ const handleKeyDown = useCallback ( ( event : KeyboardEvent ) => {
147
+ if ( event . key === "Escape" ) setIsVisible ( false ) ;
148
+ } , [ ] ) ;
80
149
81
- // Keyboard accessibility: close on Escape
150
+ // Optimized event listeners - only add when visible, use stable handlers
82
151
useEffect ( ( ) => {
83
152
if ( ! isVisible ) return ;
84
- function handleKey ( event : KeyboardEvent ) {
85
- if ( event . key === "Escape" ) setIsVisible ( false ) ;
86
- }
87
- document . addEventListener ( "keydown" , handleKey ) ;
88
- return ( ) => document . removeEventListener ( "keydown" , handleKey ) ;
89
- } , [ isVisible ] ) ;
153
+
154
+ document . addEventListener ( "mousedown" , handleClickAway ) ;
155
+ document . addEventListener ( "keydown" , handleKeyDown ) ;
156
+
157
+ return ( ) => {
158
+ document . removeEventListener ( "mousedown" , handleClickAway ) ;
159
+ document . removeEventListener ( "keydown" , handleKeyDown ) ;
160
+ } ;
161
+ } , [ isVisible , handleClickAway , handleKeyDown ] ) ;
162
+
163
+ // Memoize portal container to prevent recreation
164
+ const portalContainer = useMemo ( ( ) => getTooltipPortalContainer ( ) , [ ] ) ;
90
165
91
166
const tooltip = isVisible && tooltipPos
92
167
? createPortal (
@@ -106,14 +181,18 @@ export const ProfileInfoTooltip: FC<ProfileInfoTooltipProps> = ({ profile, class
106
181
} }
107
182
>
108
183
< div className = "space-y-1" >
109
- < div className = "flex justify-between" >
110
- < span className = "text-gray-600 dark:text-gray-400" > Port:</ span >
111
- < span className = { ClassNames . Text } > { port } </ span >
112
- </ div >
113
- < div className = "flex justify-between" >
114
- < span className = "text-gray-600 dark:text-gray-400" > Last Logged In: </ span >
115
- < span className = { ClassNames . Text } > { lastAccessed } </ span >
116
- </ div >
184
+ { port !== null && (
185
+ < div className = "flex justify-between" >
186
+ < span className = "text-gray-600 dark:text-gray-400" > Port:</ span >
187
+ < span className = { ClassNames . Text } > { port } </ span >
188
+ </ div >
189
+ ) }
190
+ { lastAccessed !== null && (
191
+ < div className = "flex justify-between" >
192
+ < span className = "text-gray-600 dark:text-gray-400" > Last Logged In: </ span >
193
+ < span className = { ClassNames . Text } > { lastAccessed } </ span >
194
+ </ div >
195
+ ) }
117
196
</ div >
118
197
< div
119
198
className = "absolute top-1/2 left-0 -translate-x-full -translate-y-1/2"
@@ -122,7 +201,7 @@ export const ProfileInfoTooltip: FC<ProfileInfoTooltipProps> = ({ profile, class
122
201
< div className = "w-0 h-0 border-t-4 border-b-4 border-r-4 border-t-transparent border-b-transparent border-r-gray-200 dark:border-r-white/20" > </ div >
123
202
</ div >
124
203
</ div > ,
125
- document . body
204
+ portalContainer
126
205
)
127
206
: null ;
128
207
@@ -144,11 +223,15 @@ export const ProfileInfoTooltip: FC<ProfileInfoTooltipProps> = ({ profile, class
144
223
) ;
145
224
} ;
146
225
147
- // Utility function to update last accessed time
226
+ // Utility function to update last accessed time with validation
148
227
export function updateProfileLastAccessed ( profileId : string ) : void {
228
+ if ( ! isValidProfileId ( profileId ) ) {
229
+ return ; // Silently fail for invalid profile IDs
230
+ }
231
+
149
232
try {
150
233
localStorage . setItem ( `whodb_profile_last_accessed_${ profileId } ` , new Date ( ) . toISOString ( ) ) ;
151
234
} catch ( error ) {
152
- console . warn ( 'Failed to update last accessed time:' , error ) ;
235
+ // Silently fail - localStorage may be full or disabled
153
236
}
154
237
}
0 commit comments