feat(03-02): create FieldRenderer and AzureAuthToggle
- FieldRenderer: registry-driven input renderer (text, password, select, hidden provider) - AzureAuthToggle: segmented SAS/Key toggle, defaults to SAS, both fields always registered via CSS show/hide - Both fields preserved on toggle (no conditional rendering that would lose react-hook-form state)
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
import type { UseFormRegister, FieldError } from 'react-hook-form';
|
||||||
|
import type { FieldDef } from '../../schemas/registry';
|
||||||
|
import { PasswordField } from './PasswordField';
|
||||||
|
|
||||||
|
interface FieldRendererProps {
|
||||||
|
field: FieldDef;
|
||||||
|
register: UseFormRegister<any>;
|
||||||
|
error?: FieldError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
||||||
|
// provider field: single-option select — hide from UI, auto-register with default value
|
||||||
|
if (field.key === 'provider' && field.options?.length === 1) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
value={field.options[0].value}
|
||||||
|
{...register(field.key)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.inputType === 'password') {
|
||||||
|
return (
|
||||||
|
<PasswordField
|
||||||
|
id={field.key}
|
||||||
|
label={field.label}
|
||||||
|
error={error}
|
||||||
|
registration={register(field.key)}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
helpText={field.helpText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.inputType === 'select' && field.options) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label htmlFor={field.key} className="text-sm font-medium text-gray-700">
|
||||||
|
{field.label}
|
||||||
|
{field.required && <span className="ml-1 text-red-500">*</span>}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id={field.key}
|
||||||
|
className={[
|
||||||
|
'w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2',
|
||||||
|
error ? 'border-red-500 focus:ring-red-300' : 'border-gray-300 focus:ring-blue-300',
|
||||||
|
].join(' ')}
|
||||||
|
{...register(field.key)}
|
||||||
|
>
|
||||||
|
{field.options.map(opt => (
|
||||||
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{field.helpText && !error && <p className="text-xs text-gray-500">{field.helpText}</p>}
|
||||||
|
{error && <p className="text-xs text-red-600">{error.message}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// text (default)
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label htmlFor={field.key} className="text-sm font-medium text-gray-700">
|
||||||
|
{field.label}
|
||||||
|
{field.required && <span className="ml-1 text-red-500">*</span>}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={field.key}
|
||||||
|
type="text"
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
className={[
|
||||||
|
'w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2',
|
||||||
|
error ? 'border-red-500 focus:ring-red-300' : 'border-gray-300 focus:ring-blue-300',
|
||||||
|
].join(' ')}
|
||||||
|
{...register(field.key)}
|
||||||
|
/>
|
||||||
|
{field.helpText && !error && <p className="text-xs text-gray-500">{field.helpText}</p>}
|
||||||
|
{error && <p className="text-xs text-red-600">{error.message}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { UseFormRegister, FieldError } from 'react-hook-form';
|
||||||
|
import { PasswordField } from '../ui/PasswordField';
|
||||||
|
|
||||||
|
type AuthMethod = 'sas' | 'key';
|
||||||
|
|
||||||
|
interface AzureAuthToggleProps {
|
||||||
|
register: UseFormRegister<any>;
|
||||||
|
errors: {
|
||||||
|
key?: FieldError;
|
||||||
|
sas_url?: FieldError;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AzureAuthToggle({ register, errors }: AzureAuthToggleProps) {
|
||||||
|
const [authMethod, setAuthMethod] = useState<AuthMethod>('sas');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{/* Segmented control */}
|
||||||
|
<div className="flex rounded-md border border-gray-300 overflow-hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAuthMethod('sas')}
|
||||||
|
className={[
|
||||||
|
'flex-1 py-1.5 text-sm font-medium transition-colors',
|
||||||
|
authMethod === 'sas'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-white text-gray-700 hover:bg-gray-50',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
SAS URL
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAuthMethod('key')}
|
||||||
|
className={[
|
||||||
|
'flex-1 py-1.5 text-sm font-medium transition-colors',
|
||||||
|
authMethod === 'key'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-white text-gray-700 hover:bg-gray-50',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
Access Key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Both fields always registered — only active one visible via CSS */}
|
||||||
|
<div className={authMethod === 'sas' ? 'block' : 'hidden'}>
|
||||||
|
<PasswordField
|
||||||
|
id="sas_url"
|
||||||
|
label="SAS URL"
|
||||||
|
error={errors.sas_url}
|
||||||
|
registration={register('sas_url')}
|
||||||
|
placeholder="https://mystorageaccount.blob.core.windows.net/?sv=..."
|
||||||
|
helpText="Full SAS URL including account and container"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={authMethod === 'key' ? 'block' : 'hidden'}>
|
||||||
|
<PasswordField
|
||||||
|
id="key"
|
||||||
|
label="Access Key"
|
||||||
|
error={errors.key}
|
||||||
|
registration={register('key')}
|
||||||
|
helpText="Base64-encoded storage account key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user