/****************************************************************************** * Copyright (c) 2026 Contributors to the Eclipse Foundation. * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * https://www.eclipse.org/legal/epl-2.0. * * SPDX-License-Identifier: EPL-2.0 *****************************************************************************/ import { ChangeEvent, FC, useCallback, useContext, useEffect, useRef, useState } from 'react'; import CheckIcon from '@mui/icons-material/Check'; import SaveIcon from '@mui/icons-material/Save'; import { Alert, Box, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Paper, Stack, Typography, } from '@mui/material'; import { MainContext } from '../../context'; import type { Settings } from '../../extension-registry-types'; import { handleError } from '../../utils'; import { SettingsItem } from './settings-item'; interface NotificationState { id: string; message: string; severity: 'error'; timeout: ReturnType; } const NOTIFICATION_TIMEOUT = 2000; const SETTINGS: Record = { readOnly: { title: 'Read-only mode', description: 'Blocks write operations while keeping browsing, search, and downloads available.', }, }; export const RuntimeSettingsPage: FC = () => { const abortController = useRef(new AbortController()); const { service } = useContext(MainContext); const [settings, setSettings] = useState(null); const [draftSettings, setDraftSettings] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [notifications, setNotifications] = useState([]); const [confirmOpen, setConfirmOpen] = useState(false); const [saveSuccess, setSaveSuccess] = useState(false); const saveSuccessTimer = useRef | null>(null); useEffect(() => { return () => abortController.current.abort(); }, []); const loadRuntimeSettings = useCallback(async () => { try { setLoading(true); setError(null); const data = await service.admin.getSettings(abortController.current); setSettings(data); setDraftSettings(data); } catch (err) { setError(handleError(err as Error)); } finally { setLoading(false); } }, [service, error]); useEffect(() => { loadRuntimeSettings(); }, [loadRuntimeSettings]); useEffect(() => () => { notifications.forEach(n => clearTimeout(n.timeout)); }, []); useEffect(() => () => { if (saveSuccessTimer.current) clearTimeout(saveSuccessTimer.current); }, []); const addNotification = useCallback((notification: Pick) => { const id = crypto.randomUUID(); const timeout = setTimeout(() => { setNotifications(current => current.filter(n => n.id !== id)); }, NOTIFICATION_TIMEOUT); setNotifications(current => [...current, { ...notification, severity: 'error', id, timeout }]); }, []); const handleNotificationClose = (id: string) => { setNotifications(current => { const notification = current.find(n => n.id === id); if (notification) clearTimeout(notification.timeout); return current.filter(n => n.id !== id); }); }; const handleFlagChange = useCallback((key: keyof Settings) => (_event: ChangeEvent, checked: boolean) => { setDraftSettings(current => current ? { ...current, [key]: checked } : current); setSaveSuccess(false); }, []); const hasChanges = draftSettings !== null && settings !== null && (Object.keys(SETTINGS) as (keyof Settings)[]).some(k => draftSettings[k] !== settings[k]); const handleSaveClick = () => setConfirmOpen(true); const handleConfirmClose = () => setConfirmOpen(false); const handleConfirmSave = useCallback(async () => { if (!draftSettings) return; setConfirmOpen(false); setSaving(true); setError(null); try { const updatedSettings = await service.admin.updateSettings(abortController.current, draftSettings); setSettings(updatedSettings); setDraftSettings(updatedSettings); setSaveSuccess(true); if (saveSuccessTimer.current) clearTimeout(saveSuccessTimer.current); saveSuccessTimer.current = setTimeout(() => setSaveSuccess(false), 2000); } catch (err) { addNotification({ message: `Failed to save runtime settings. ${handleError(err as Error)}`, }); } finally { setSaving(false); } }, [draftSettings, service, addNotification]); return ( <> Settings Manage runtime settings that apply across the registry. {error && ( setError(null)}> {error} )} {(Object.entries(SETTINGS) as [keyof Settings, { title: string; description: string }][]).map(([key, flag]) => ( ))} Apply settings? These changes will be applied immediately and will affect all users of the registry. Make sure you understand the impact before proceeding. {notifications.length > 0 && ( theme.zIndex.snackbar, width: 'min(420px, calc(100vw - 32px))', }} > {notifications.map(notification => ( handleNotificationClose(notification.id)} severity={notification.severity} variant='filled' sx={{ width: '100%' }} > {notification.message} ))} )} ); };