Co-authored-by: Claude Haiku 4.5 <[email protected]>
This commit is contained in:
Anthony Stirling
2026-03-02 13:56:39 +00:00
committed by GitHub
co-authored by Claude Haiku 4.5
parent 8b25db37ad
commit 012bd1af92
21 changed files with 407 additions and 66 deletions
@@ -0,0 +1,118 @@
import { useState, useRef, useEffect } from 'react';
import { PasswordInput, Group, ActionIcon, Tooltip, TextInput } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import LocalIcon from '@app/components/shared/LocalIcon';
interface EditableSecretFieldProps {
label?: string;
description?: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
disabled?: boolean;
error?: string;
}
/**
* Component for editing sensitive fields (passwords, API keys, secrets).
*
* UX:
* - Normal password input in all scenarios EXCEPT when value is masked (********)
* - When backend returns masked value (********): Shows read-only display + Edit button
* - Click Edit to change the masked value
*/
export default function EditableSecretField({
label,
description,
value,
onChange,
placeholder = 'Enter value',
disabled = false,
error,
}: EditableSecretFieldProps) {
const { t } = useTranslation();
const [isEditing, setIsEditing] = useState(false);
const [tempValue, setTempValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const isMasked = value === '********';
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
const handleEdit = () => {
setTempValue('');
setIsEditing(true);
};
const handleCancel = () => {
setTempValue('');
setIsEditing(false);
};
const handleSave = () => {
if (tempValue.trim() !== '') {
onChange(tempValue);
}
setTempValue('');
setIsEditing(false);
};
return (
<div>
{label && <label style={{ display: 'block', marginBottom: 4, fontWeight: 500, fontSize: '0.875rem' }}>{label}</label>}
{description && <p style={{ margin: '4px 0 12px 0', fontSize: '0.75rem', color: '#666' }}>{description}</p>}
{isMasked && !isEditing ? (
// Masked value from backend: show display + Edit button
<Group gap="xs" align="flex-end">
<TextInput
value="••••••••"
disabled
style={{ flex: 1 }}
readOnly
/>
<Tooltip label={t('editSecret')} withArrow>
<ActionIcon
variant="light"
onClick={handleEdit}
disabled={disabled}
title="Edit"
aria-label="Edit secret value"
>
<LocalIcon icon="edit" width="1rem" height="1rem" />
</ActionIcon>
</Tooltip>
</Group>
) : isEditing ? (
// Edit mode: normal password input
<PasswordInput
ref={inputRef}
value={tempValue}
onChange={(e) => setTempValue(e.currentTarget.value)}
placeholder={placeholder}
disabled={disabled}
error={error}
autoComplete="new-password"
onBlur={handleSave}
onKeyDown={(e) => {
if (e.key === 'Escape') handleCancel();
}}
/>
) : (
// Normal password input: empty or user typing
<PasswordInput
value={value}
onChange={(e) => onChange(e.currentTarget.value)}
placeholder={placeholder}
disabled={disabled}
error={error}
autoComplete="new-password"
/>
)}
</div>
);
}
@@ -9,12 +9,12 @@ import {
TextInput,
Textarea,
Switch,
PasswordInput,
NumberInput,
TagsInput,
} from '@mantine/core';
import { useTranslation } from 'react-i18next';
import LocalIcon from '@app/components/shared/LocalIcon';
import EditableSecretField from '@app/components/shared/EditableSecretField';
import { Provider, ProviderField } from '@app/components/shared/config/configSections/providerDefinitions';
interface ProviderCardProps {
@@ -94,13 +94,13 @@ export default function ProviderCard({
case 'password':
return (
<PasswordInput
<EditableSecretField
key={field.key}
label={field.label}
description={field.description}
placeholder={field.placeholder}
value={value}
onChange={(e) => handleFieldChange(field.key, e.target.value)}
onChange={(newValue) => handleFieldChange(field.key, newValue)}
disabled={disabled}
/>
);