Wire Azure OCR to central settings toggle; remove redundant Users section
- settings/src/integrations/schema.js: rename azure from 'Azure AD' to 'Azure Document Intelligence', swap fields to endpoint + api_key - Add use_global_azure column (migration + model) - global_settings_service: add azure to check_global_status and apply_global_overrides - api/settings.py: expose use_global_azure in response/update; apply overrides in test_azure_connection before credential check - Settings.tsx: add 'Use credentials from main stack settings' toggle for Azure OCR section (endpoint/key disabled when on, test button enabled when global is configured); remove Users section (managed centrally via auth service), clean up UserData interface, users query and mutations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d7898ba897
commit
1564a66283
5 changed files with 44 additions and 161 deletions
|
|
@ -51,6 +51,7 @@ class SettingsResponse(BaseModel):
|
||||||
llm_features_enabled: dict | None = None
|
llm_features_enabled: dict | None = None
|
||||||
# Global stack settings delegation
|
# Global stack settings delegation
|
||||||
use_global_smtp: bool = False
|
use_global_smtp: bool = False
|
||||||
|
use_global_azure: bool = False
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
@ -96,6 +97,7 @@ class SettingsUpdate(BaseModel):
|
||||||
llm_features_enabled: dict | None = None
|
llm_features_enabled: dict | None = None
|
||||||
# Global stack settings delegation
|
# Global stack settings delegation
|
||||||
use_global_smtp: bool | None = None
|
use_global_smtp: bool | None = None
|
||||||
|
use_global_azure: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=SettingsResponse)
|
@router.get("/", response_model=SettingsResponse)
|
||||||
|
|
@ -163,6 +165,7 @@ def _build_settings_response(settings: KitchenSettings) -> SettingsResponse:
|
||||||
llm_monthly_token_limit=settings.llm_monthly_token_limit,
|
llm_monthly_token_limit=settings.llm_monthly_token_limit,
|
||||||
llm_features_enabled=settings.llm_features_enabled,
|
llm_features_enabled=settings.llm_features_enabled,
|
||||||
use_global_smtp=settings.use_global_smtp,
|
use_global_smtp=settings.use_global_smtp,
|
||||||
|
use_global_azure=settings.use_global_azure,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -205,7 +208,13 @@ async def test_azure_connection(
|
||||||
)
|
)
|
||||||
settings = result.scalar_one_or_none()
|
settings = result.scalar_one_or_none()
|
||||||
|
|
||||||
if not settings or not settings.azure_endpoint or not settings.azure_key:
|
if not settings:
|
||||||
|
raise HTTPException(status_code=400, detail="Azure credentials not configured")
|
||||||
|
|
||||||
|
from services.global_settings_service import apply_global_overrides
|
||||||
|
await apply_global_overrides(settings)
|
||||||
|
|
||||||
|
if not settings.azure_endpoint or not settings.azure_key:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Azure credentials not configured"
|
detail="Azure credentials not configured"
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ async def migrate():
|
||||||
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS use_global_newbook BOOLEAN DEFAULT FALSE",
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS use_global_newbook BOOLEAN DEFAULT FALSE",
|
||||||
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS use_global_resos BOOLEAN DEFAULT FALSE",
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS use_global_resos BOOLEAN DEFAULT FALSE",
|
||||||
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS use_global_sambapos BOOLEAN DEFAULT FALSE",
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS use_global_sambapos BOOLEAN DEFAULT FALSE",
|
||||||
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS use_global_azure BOOLEAN DEFAULT FALSE",
|
||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
await conn.execute(text(col))
|
await conn.execute(text(col))
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,7 @@ class KitchenSettings(Base):
|
||||||
use_global_newbook: Mapped[bool] = mapped_column(Boolean, default=False)
|
use_global_newbook: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
use_global_resos: Mapped[bool] = mapped_column(Boolean, default=False)
|
use_global_resos: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
use_global_sambapos: Mapped[bool] = mapped_column(Boolean, default=False)
|
use_global_sambapos: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
use_global_azure: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
# Internal API key (for in-house apps like menu display plugin)
|
# Internal API key (for in-house apps like menu display plugin)
|
||||||
api_key: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
api_key: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ async def check_global_status() -> dict[str, bool]:
|
||||||
"newbook": ["api_key"],
|
"newbook": ["api_key"],
|
||||||
"resos": ["api_key"],
|
"resos": ["api_key"],
|
||||||
"sambapos": ["password"],
|
"sambapos": ["password"],
|
||||||
|
"azure": ["api_key"],
|
||||||
}
|
}
|
||||||
results: dict[str, bool] = {}
|
results: dict[str, bool] = {}
|
||||||
for slug, fields in required.items():
|
for slug, fields in required.items():
|
||||||
|
|
@ -95,3 +96,9 @@ async def apply_global_overrides(settings) -> None:
|
||||||
settings.sambapos_db_name = creds.get("database") or settings.sambapos_db_name
|
settings.sambapos_db_name = creds.get("database") or settings.sambapos_db_name
|
||||||
settings.sambapos_db_username = creds.get("username") or settings.sambapos_db_username
|
settings.sambapos_db_username = creds.get("username") or settings.sambapos_db_username
|
||||||
settings.sambapos_db_password = creds.get("password") or settings.sambapos_db_password
|
settings.sambapos_db_password = creds.get("password") or settings.sambapos_db_password
|
||||||
|
|
||||||
|
if getattr(settings, "use_global_azure", False):
|
||||||
|
creds = await get_global_integration("azure")
|
||||||
|
if creds:
|
||||||
|
settings.azure_endpoint = creds.get("endpoint") or settings.azure_endpoint
|
||||||
|
settings.azure_key = creds.get("api_key") or settings.azure_key
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ interface SettingsData {
|
||||||
llm_monthly_token_limit: number
|
llm_monthly_token_limit: number
|
||||||
llm_features_enabled: Record<string, boolean> | null
|
llm_features_enabled: Record<string, boolean> | null
|
||||||
use_global_smtp: boolean
|
use_global_smtp: boolean
|
||||||
|
use_global_azure: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NewbookSettingsData {
|
interface NewbookSettingsData {
|
||||||
|
|
@ -105,16 +106,7 @@ interface RoomCategory {
|
||||||
display_order: number
|
display_order: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserData {
|
type SettingsSection = 'account' | 'access' | 'display' | 'azure' | 'email' | 'inbox' | 'dext' | 'newbook' | 'resos' | 'sambapos' | 'kds' | 'budget' | 'kitchen' | 'suppliers' | 'search' | 'nextcloud' | 'backup' | 'food_flags' | 'allergen_keywords' | 'ingredient_categories' | 'recipe_sections' | 'dish_courses' | 'api_access' | 'llm' | 'data'
|
||||||
id: number
|
|
||||||
email: string
|
|
||||||
name: string | null
|
|
||||||
is_active: boolean
|
|
||||||
is_admin: boolean
|
|
||||||
created_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SettingsSection = 'account' | 'users' | 'access' | 'display' | 'azure' | 'email' | 'inbox' | 'dext' | 'newbook' | 'resos' | 'sambapos' | 'kds' | 'budget' | 'kitchen' | 'suppliers' | 'search' | 'nextcloud' | 'backup' | 'food_flags' | 'allergen_keywords' | 'ingredient_categories' | 'recipe_sections' | 'dish_courses' | 'api_access' | 'llm' | 'data'
|
|
||||||
|
|
||||||
interface SambaPOSSettingsData {
|
interface SambaPOSSettingsData {
|
||||||
sambapos_db_host: string | null
|
sambapos_db_host: string | null
|
||||||
|
|
@ -290,6 +282,7 @@ export default function Settings() {
|
||||||
const [azureEndpoint, setAzureEndpoint] = useState('')
|
const [azureEndpoint, setAzureEndpoint] = useState('')
|
||||||
const [azureKey, setAzureKey] = useState('')
|
const [azureKey, setAzureKey] = useState('')
|
||||||
const [azureTestStatus, setAzureTestStatus] = useState<string | null>(null)
|
const [azureTestStatus, setAzureTestStatus] = useState<string | null>(null)
|
||||||
|
const [useGlobalAzure, setUseGlobalAzure] = useState(false)
|
||||||
// OCR post-processing options
|
// OCR post-processing options
|
||||||
const [ocrCleanProductCodes, setOcrCleanProductCodes] = useState(false)
|
const [ocrCleanProductCodes, setOcrCleanProductCodes] = useState(false)
|
||||||
const [ocrFilterSubtotalRows, setOcrFilterSubtotalRows] = useState(false)
|
const [ocrFilterSubtotalRows, setOcrFilterSubtotalRows] = useState(false)
|
||||||
|
|
@ -611,21 +604,6 @@ export default function Settings() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Fetch users (admin only)
|
|
||||||
const { data: users } = useQuery<UserData[]>({
|
|
||||||
queryKey: ['users'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await fetch('/auth/users', {
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
if (res.status === 403) return []
|
|
||||||
throw new Error('Failed to fetch users')
|
|
||||||
}
|
|
||||||
return res.json()
|
|
||||||
},
|
|
||||||
enabled: !!user?.is_admin,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Fetch SambaPOS settings
|
// Fetch SambaPOS settings
|
||||||
const { data: sambaSettings } = useQuery<SambaPOSSettingsData>({
|
const { data: sambaSettings } = useQuery<SambaPOSSettingsData>({
|
||||||
|
|
@ -905,6 +883,7 @@ export default function Settings() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (settings) {
|
if (settings) {
|
||||||
setAzureEndpoint(settings.azure_endpoint || '')
|
setAzureEndpoint(settings.azure_endpoint || '')
|
||||||
|
setUseGlobalAzure(settings.use_global_azure)
|
||||||
setCurrencySymbol(settings.currency_symbol)
|
setCurrencySymbol(settings.currency_symbol)
|
||||||
setDateFormat(settings.date_format)
|
setDateFormat(settings.date_format)
|
||||||
setHighQuantityThreshold(settings.high_quantity_threshold)
|
setHighQuantityThreshold(settings.high_quantity_threshold)
|
||||||
|
|
@ -1662,57 +1641,6 @@ export default function Settings() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggleUserMutation = useMutation({
|
|
||||||
mutationFn: async (userId: number) => {
|
|
||||||
const res = await fetch(`/auth/users/${userId}/toggle-active`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
const data = await res.json()
|
|
||||||
throw new Error(data.detail || 'Failed to toggle user')
|
|
||||||
}
|
|
||||||
return res.json()
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteUserMutation = useMutation({
|
|
||||||
mutationFn: async (userId: number) => {
|
|
||||||
const res = await fetch(`/auth/users/${userId}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
const data = await res.json()
|
|
||||||
throw new Error(data.detail || 'Failed to delete user')
|
|
||||||
}
|
|
||||||
return res.json()
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const toggleAdminMutation = useMutation({
|
|
||||||
mutationFn: async (userId: number) => {
|
|
||||||
const res = await fetch(`/auth/users/${userId}/toggle-admin`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
const data = await res.json()
|
|
||||||
throw new Error(data.detail || 'Failed to toggle admin status')
|
|
||||||
}
|
|
||||||
return res.json()
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const savePageRestrictionsMutation = useMutation({
|
const savePageRestrictionsMutation = useMutation({
|
||||||
mutationFn: async (pages: string[]) => {
|
mutationFn: async (pages: string[]) => {
|
||||||
const res = await fetch('/kitchen/api/settings/page-restrictions', {
|
const res = await fetch('/kitchen/api/settings/page-restrictions', {
|
||||||
|
|
@ -2543,6 +2471,7 @@ export default function Settings() {
|
||||||
ocr_use_weight_as_quantity: ocrUseWeightAsQuantity,
|
ocr_use_weight_as_quantity: ocrUseWeightAsQuantity,
|
||||||
cost_distribution_max_days: costDistMaxDays,
|
cost_distribution_max_days: costDistMaxDays,
|
||||||
use_global_smtp: useGlobalSmtp,
|
use_global_smtp: useGlobalSmtp,
|
||||||
|
use_global_azure: useGlobalAzure,
|
||||||
}
|
}
|
||||||
if (azureKey) {
|
if (azureKey) {
|
||||||
data.azure_key = azureKey
|
data.azure_key = azureKey
|
||||||
|
|
@ -2758,7 +2687,6 @@ export default function Settings() {
|
||||||
|
|
||||||
const sidebarItems: { id: SettingsSection; label: string; adminOnly?: boolean; href?: string; restrictPath?: string }[] = [
|
const sidebarItems: { id: SettingsSection; label: string; adminOnly?: boolean; href?: string; restrictPath?: string }[] = [
|
||||||
{ id: 'account', label: 'Account' },
|
{ id: 'account', label: 'Account' },
|
||||||
{ id: 'users', label: 'Users', adminOnly: true, restrictPath: '/settings-users' },
|
|
||||||
{ id: 'access', label: 'Access Control', adminOnly: true, restrictPath: '/settings-access' },
|
{ id: 'access', label: 'Access Control', adminOnly: true, restrictPath: '/settings-access' },
|
||||||
{ id: 'display', label: 'Display', restrictPath: '/settings-display' },
|
{ id: 'display', label: 'Display', restrictPath: '/settings-display' },
|
||||||
{ id: 'azure', label: 'Azure OCR', restrictPath: '/settings-azure' },
|
{ id: 'azure', label: 'Azure OCR', restrictPath: '/settings-azure' },
|
||||||
|
|
@ -2933,82 +2861,6 @@ export default function Settings() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Users Section (Admin Only) */}
|
|
||||||
{activeSection === 'users' && user?.is_admin && (
|
|
||||||
<div style={styles.section}>
|
|
||||||
<h2 style={styles.sectionTitle}>User Management</h2>
|
|
||||||
<p style={styles.hint}>Manage users who have access to this kitchen.</p>
|
|
||||||
|
|
||||||
{/* Users List Block */}
|
|
||||||
<div style={styles.settingsBlock}>
|
|
||||||
<h3 style={styles.blockTitle}>Users</h3>
|
|
||||||
{users && users.length > 0 && (
|
|
||||||
<table style={styles.table}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style={styles.th}>Email</th>
|
|
||||||
<th style={styles.th}>Name</th>
|
|
||||||
<th style={styles.th}>Status</th>
|
|
||||||
<th style={styles.th}>Role</th>
|
|
||||||
<th style={styles.th}>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{users.map((u) => (
|
|
||||||
<tr key={u.id} style={!u.is_active ? styles.disabledRow : undefined}>
|
|
||||||
<td style={styles.td}>{u.email}</td>
|
|
||||||
<td style={styles.td}>{u.name || '-'}</td>
|
|
||||||
<td style={styles.td}>
|
|
||||||
<span style={u.is_active ? styles.activeStatus : styles.inactiveStatus}>
|
|
||||||
{u.is_active ? 'Active' : 'Disabled'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td style={styles.td}>{u.is_admin ? 'Admin' : 'User'}</td>
|
|
||||||
<td style={styles.td}>
|
|
||||||
{u.id !== user.id ? (
|
|
||||||
<div style={styles.actionButtons}>
|
|
||||||
<button
|
|
||||||
onClick={() => toggleUserMutation.mutate(u.id)}
|
|
||||||
style={u.is_active ? styles.disableBtn : styles.enableBtn}
|
|
||||||
>
|
|
||||||
{u.is_active ? 'Disable' : 'Enable'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (confirm(`${u.is_admin ? 'Remove admin rights from' : 'Make admin'} ${u.email}?`)) {
|
|
||||||
toggleAdminMutation.mutate(u.id)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={u.is_admin ? styles.demoteBtn : styles.promoteBtn}
|
|
||||||
>
|
|
||||||
{u.is_admin ? 'Demote' : 'Make Admin'}
|
|
||||||
</button>
|
|
||||||
{!u.is_admin && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (confirm(`Delete user ${u.email}?`)) {
|
|
||||||
deleteUserMutation.mutate(u.id)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={styles.deleteBtn}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span style={styles.youLabel}>(You)</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Access Control Section (Admin Only) */}
|
{/* Access Control Section (Admin Only) */}
|
||||||
{activeSection === 'access' && user?.is_admin && (
|
{activeSection === 'access' && user?.is_admin && (
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
|
|
@ -3110,7 +2962,6 @@ export default function Settings() {
|
||||||
<div style={styles.checkboxGroup}>
|
<div style={styles.checkboxGroup}>
|
||||||
{[
|
{[
|
||||||
{ path: '/settings', label: 'Settings Page (entire page)' },
|
{ path: '/settings', label: 'Settings Page (entire page)' },
|
||||||
{ path: '/settings-users', label: 'Users Management' },
|
|
||||||
{ path: '/settings-access', label: 'Access Control' },
|
{ path: '/settings-access', label: 'Access Control' },
|
||||||
{ path: '/settings-display', label: 'Display Settings' },
|
{ path: '/settings-display', label: 'Display Settings' },
|
||||||
{ path: '/settings-azure', label: 'Azure OCR' },
|
{ path: '/settings-azure', label: 'Azure OCR' },
|
||||||
|
|
@ -3262,6 +3113,21 @@ export default function Settings() {
|
||||||
{/* API Configuration Block */}
|
{/* API Configuration Block */}
|
||||||
<div style={styles.settingsBlock}>
|
<div style={styles.settingsBlock}>
|
||||||
<h3 style={styles.blockTitle}>API Configuration</h3>
|
<h3 style={styles.blockTitle}>API Configuration</h3>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '1rem', cursor: 'pointer' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={useGlobalAzure}
|
||||||
|
onChange={(e) => setUseGlobalAzure(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span style={{ fontWeight: 500 }}>Use credentials from main stack settings</span>
|
||||||
|
{useGlobalAzure && (
|
||||||
|
<span style={{ fontSize: '0.8rem', padding: '0.15rem 0.5rem', borderRadius: '10px',
|
||||||
|
background: globalStatus?.azure ? '#d4edda' : '#fff3cd',
|
||||||
|
color: globalStatus?.azure ? '#155724' : '#856404' }}>
|
||||||
|
{globalStatus?.azure ? 'Configured' : 'Not configured in stack settings'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
<div style={styles.form}>
|
<div style={styles.form}>
|
||||||
<label style={styles.label}>
|
<label style={styles.label}>
|
||||||
Endpoint URL
|
Endpoint URL
|
||||||
|
|
@ -3269,7 +3135,8 @@ export default function Settings() {
|
||||||
type="text"
|
type="text"
|
||||||
value={azureEndpoint}
|
value={azureEndpoint}
|
||||||
onChange={(e) => setAzureEndpoint(e.target.value)}
|
onChange={(e) => setAzureEndpoint(e.target.value)}
|
||||||
style={styles.input}
|
disabled={useGlobalAzure}
|
||||||
|
style={{ ...styles.input, ...(useGlobalAzure ? { opacity: 0.4, pointerEvents: 'none' as const } : {}) }}
|
||||||
placeholder="https://your-resource.cognitiveservices.azure.com/"
|
placeholder="https://your-resource.cognitiveservices.azure.com/"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -3279,15 +3146,13 @@ export default function Settings() {
|
||||||
type="password"
|
type="password"
|
||||||
value={azureKey}
|
value={azureKey}
|
||||||
onChange={(e) => setAzureKey(e.target.value)}
|
onChange={(e) => setAzureKey(e.target.value)}
|
||||||
style={styles.input}
|
disabled={useGlobalAzure}
|
||||||
|
style={{ ...styles.input, ...(useGlobalAzure ? { opacity: 0.4, pointerEvents: 'none' as const } : {}) }}
|
||||||
placeholder={settings?.azure_key_set ? '••••••••••••••••' : 'Enter your API key'}
|
placeholder={settings?.azure_key_set ? '••••••••••••••••' : 'Enter your API key'}
|
||||||
/>
|
/>
|
||||||
{settings?.azure_key_set && !azureKey && <span style={styles.keyStatus}>Key is configured</span>}
|
{settings?.azure_key_set && !azureKey && <span style={styles.keyStatus}>Key is configured</span>}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ ...styles.hint, marginTop: '0.75rem' }}>
|
|
||||||
Azure Document Intelligence uses app-specific credentials (not shared with the main stack).
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* OCR Post-Processing Block */}
|
{/* OCR Post-Processing Block */}
|
||||||
|
|
@ -3328,7 +3193,7 @@ export default function Settings() {
|
||||||
|
|
||||||
{/* Actions - outside blocks */}
|
{/* Actions - outside blocks */}
|
||||||
<div style={styles.buttonRow}>
|
<div style={styles.buttonRow}>
|
||||||
<button onClick={() => azureTestMutation.mutate()} style={styles.testBtn} disabled={!settings?.azure_key_set}>
|
<button onClick={() => azureTestMutation.mutate()} style={styles.testBtn} disabled={!settings?.azure_key_set && !(useGlobalAzure && globalStatus?.azure)}>
|
||||||
Test Connection
|
Test Connection
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleSaveSettings} style={styles.saveBtn} disabled={updateMutation.isPending}>
|
<button onClick={handleSaveSettings} style={styles.saveBtn} disabled={updateMutation.isPending}>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue