fix(uptime): fix TCP monitor UX — correct format guidance and add client-side validation
The TCP monitor creation form showed a placeholder that instructed users to enter a URL with the tcp:// scheme prefix (e.g., tcp://192.168.1.1:8080). Following this guidance caused a silent HTTP 500 error because Go's net.SplitHostPort rejects any input containing a scheme prefix, expecting bare host:port format only. - Corrected the urlPlaceholder translation key to remove the tcp:// prefix - Added per-type dynamic placeholder (urlPlaceholderHttp / urlPlaceholderTcp) so the URL input shows the correct example format as soon as the user selects a monitor type - Added per-type helper text below the URL input explaining the required format, updated in real time when the type selector changes - Added client-side validation: typing a scheme prefix (://) in TCP mode shows an inline error and blocks form submission before the request reaches the backend - Reordered the Create Monitor form so the type selector appears before the URL input, giving users the correct format context before they type - Type selector onChange now clears any stale urlError to prevent incorrect error messages persisting after switching from TCP back to HTTP - Added 5 new i18n keys across all 5 supported locales (en, de, fr, es, zh) - Added 10 RTL unit tests covering all new validation paths including the type-change error-clear scenario - Added 9 Playwright E2E tests covering placeholder variants, helper text, inline error lifecycle, submission blocking, and successful TCP creation Closes #issue-5 (TCP monitor UI cannot add monitor when following placeholder)
This commit is contained in:
@@ -346,6 +346,11 @@ const CreateMonitorModal: FC<{ onClose: () => void; t: (key: string) => string }
|
||||
const [type, setType] = useState<'http' | 'tcp'>('http');
|
||||
const [interval, setInterval] = useState(60);
|
||||
const [maxRetries, setMaxRetries] = useState(3);
|
||||
const [urlError, setUrlError] = useState('');
|
||||
|
||||
const urlPlaceholder = type === 'tcp'
|
||||
? t('uptime.urlPlaceholderTcp')
|
||||
: t('uptime.urlPlaceholderHttp');
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: { name: string; url: string; type: string; interval?: number; max_retries?: number }) =>
|
||||
@@ -363,6 +368,10 @@ const CreateMonitorModal: FC<{ onClose: () => void; t: (key: string) => string }
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim() || !url.trim()) return;
|
||||
if (type === 'tcp' && url.trim().includes('://')) {
|
||||
setUrlError(t('uptime.invalidTcpFormat'));
|
||||
return;
|
||||
}
|
||||
mutation.mutate({ name: name.trim(), url: url.trim(), type, interval, max_retries: maxRetries });
|
||||
};
|
||||
|
||||
@@ -399,6 +408,24 @@ const CreateMonitorModal: FC<{ onClose: () => void; t: (key: string) => string }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="create-monitor-type" className="block text-sm font-medium text-gray-300 mb-1">
|
||||
{t('uptime.monitorType')} *
|
||||
</label>
|
||||
<select
|
||||
id="create-monitor-type"
|
||||
value={type}
|
||||
onChange={(e) => {
|
||||
setType(e.target.value as 'http' | 'tcp');
|
||||
setUrlError('');
|
||||
}}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="http">{t('uptime.monitorTypeHttp')}</option>
|
||||
<option value="tcp">{t('uptime.monitorTypeTcp')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="create-monitor-url" className="block text-sm font-medium text-gray-300 mb-1">
|
||||
{t('uptime.monitorUrl')} *
|
||||
@@ -407,26 +434,35 @@ const CreateMonitorModal: FC<{ onClose: () => void; t: (key: string) => string }
|
||||
id="create-monitor-url"
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setUrl(val);
|
||||
if (type === 'tcp' && val.includes('://')) {
|
||||
setUrlError(t('uptime.invalidTcpFormat'));
|
||||
} else {
|
||||
setUrlError('');
|
||||
}
|
||||
}}
|
||||
required
|
||||
aria-describedby={`create-monitor-url-helper${urlError ? ' create-monitor-url-error' : ''}`}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder={t('uptime.urlPlaceholder')}
|
||||
placeholder={urlPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="create-monitor-type" className="block text-sm font-medium text-gray-300 mb-1">
|
||||
{t('uptime.monitorType')} *
|
||||
</label>
|
||||
<select
|
||||
id="create-monitor-type"
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value as 'http' | 'tcp')}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
<p
|
||||
id="create-monitor-url-helper"
|
||||
className="text-xs text-gray-400 mt-1"
|
||||
>
|
||||
<option value="http">{t('uptime.monitorTypeHttp')}</option>
|
||||
<option value="tcp">{t('uptime.monitorTypeTcp')}</option>
|
||||
</select>
|
||||
{type === 'tcp' ? t('uptime.urlHelperTcp') : t('uptime.urlHelperHttp')}
|
||||
</p>
|
||||
{urlError && (
|
||||
<p
|
||||
id="create-monitor-url-error"
|
||||
className="text-xs text-red-400 mt-1"
|
||||
role="alert"
|
||||
>
|
||||
{urlError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user