Separate panel settings from services

This commit is contained in:
2026-04-02 01:34:16 +03:00
parent dc9e399e5b
commit 1bd7ce2ec3
5 changed files with 56 additions and 46 deletions

View File

@@ -34,3 +34,4 @@ Updated: 2026-04-02
18. Restored panel language/theme preferences in Settings with `localStorage`, merged service `Command/Protocol` into a single `Type`, and removed the legacy `admin` service path from managed panel state. 18. Restored panel language/theme preferences in Settings with `localStorage`, merged service `Command/Protocol` into a single `Type`, and removed the legacy `admin` service path from managed panel state.
19. Added service-removal confirmation with linked-user warnings, backend cascade deletion for removed services, and migration that strips persisted legacy `admin` services from stored state. 19. Added service-removal confirmation with linked-user warnings, backend cascade deletion for removed services, and migration that strips persisted legacy `admin` services from stored state.
20. Made `npm run dev` start both the Vite client and Express backend, added a Vite API proxy for local development, and restored `system` as the default panel theme so the login screen follows OS appearance. 20. Made `npm run dev` start both the Vite client and Express backend, added a Vite API proxy for local development, and restored `system` as the default panel theme so the login screen follows OS appearance.
21. Re-separated the Settings tab into distinct panel-settings and services cards so panel preferences no longer appear inside the Services section.

View File

@@ -24,7 +24,7 @@ Updated: 2026-04-02
- `src/main.tsx`: application bootstrap - `src/main.tsx`: application bootstrap
- `src/App.tsx`: authenticated panel shell with API-backed login, `sessionStorage` token persistence, localized labels, early theme application, and protected panel mutations - `src/App.tsx`: authenticated panel shell with API-backed login, `sessionStorage` token persistence, localized labels, early theme application, and protected panel mutations
- `src/SystemTab.tsx`: Settings tab with panel language/style preferences, unified service type editing, remove confirmation, and generated config preview - `src/SystemTab.tsx`: Settings tab with separate panel-settings and services cards, unified service type editing, remove confirmation, and generated config preview
- `src/App.test.tsx`: login-gate, preferences persistence, modal interaction, pause/resume, delete-confirm, and settings-save UI tests - `src/App.test.tsx`: login-gate, preferences persistence, modal interaction, pause/resume, delete-confirm, and settings-save UI tests
- `src/app.css`: full panel styling - `src/app.css`: full panel styling
- `src/data/mockDashboard.ts`: default panel state and frontend fallback snapshot - `src/data/mockDashboard.ts`: default panel state and frontend fallback snapshot

View File

@@ -149,7 +149,8 @@ describe('App login gate', () => {
await loginIntoPanel(user); await loginIntoPanel(user);
await user.click(screen.getByRole('button', { name: /settings/i })); await user.click(screen.getByRole('button', { name: /settings/i }));
expect(screen.queryByText(/panel settings/i)).not.toBeInTheDocument(); expect(screen.getByRole('heading', { name: /panel settings/i })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /^services$/i })).toBeInTheDocument();
expect(screen.queryByLabelText(/public host/i)).not.toBeInTheDocument(); expect(screen.queryByLabelText(/public host/i)).not.toBeInTheDocument();
expect(screen.queryByLabelText(/command/i)).not.toBeInTheDocument(); expect(screen.queryByLabelText(/command/i)).not.toBeInTheDocument();
expect(screen.queryByLabelText(/protocol/i)).not.toBeInTheDocument(); expect(screen.queryByLabelText(/protocol/i)).not.toBeInTheDocument();

View File

@@ -73,52 +73,58 @@ export default function SystemTab({
<section className="page-grid single-column system-grid"> <section className="page-grid single-column system-grid">
<article className="panel-card"> <article className="panel-card">
<div className="card-header"> <div className="card-header">
<h2>{text.settings.title}</h2> <h2>{text.settings.panelTitle}</h2>
<div className="settings-toolbar"> </div>
<label className="field-group compact-field"> <div className="settings-toolbar">
<span>{text.common.language}</span> <label className="field-group compact-field">
<select <span>{text.common.language}</span>
value={preferences.language} <select
onChange={(event) => value={preferences.language}
onPreferencesChange({ onChange={(event) =>
...preferences, onPreferencesChange({
language: event.target.value as PanelLanguage, ...preferences,
}) language: event.target.value as PanelLanguage,
} })
>
<option value="en">{text.common.english}</option>
<option value="ru">{text.common.russian}</option>
</select>
</label>
<label className="field-group compact-field">
<span>{text.common.theme}</span>
<select
value={preferences.theme}
onChange={(event) =>
onPreferencesChange({
...preferences,
theme: event.target.value as PanelTheme,
})
}
>
<option value="light">{getThemeLabel(preferences.language, 'light')}</option>
<option value="dark">{getThemeLabel(preferences.language, 'dark')}</option>
<option value="system">{getThemeLabel(preferences.language, 'system')}</option>
</select>
</label>
<button
type="button"
className="button-secondary"
onClick={() =>
setDraft((current) => ({
...current,
services: [...current.services, createServiceDraft(current.services)],
}))
} }
> >
{text.common.addService} <option value="en">{text.common.english}</option>
</button> <option value="ru">{text.common.russian}</option>
</div> </select>
</label>
<label className="field-group compact-field">
<span>{text.common.theme}</span>
<select
value={preferences.theme}
onChange={(event) =>
onPreferencesChange({
...preferences,
theme: event.target.value as PanelTheme,
})
}
>
<option value="light">{getThemeLabel(preferences.language, 'light')}</option>
<option value="dark">{getThemeLabel(preferences.language, 'dark')}</option>
<option value="system">{getThemeLabel(preferences.language, 'system')}</option>
</select>
</label>
</div>
</article>
<article className="panel-card">
<div className="card-header">
<h2>{text.settings.title}</h2>
<button
type="button"
className="button-secondary"
onClick={() =>
setDraft((current) => ({
...current,
services: [...current.services, createServiceDraft(current.services)],
}))
}
>
{text.common.addService}
</button>
</div> </div>
<div className="service-editor-list"> <div className="service-editor-list">
{draft.services.map((service, index) => ( {draft.services.map((service, index) => (

View File

@@ -92,6 +92,7 @@ const text = {
deleteAction: 'Delete user', deleteAction: 'Delete user',
}, },
settings: { settings: {
panelTitle: 'Panel settings',
title: 'Services', title: 'Services',
generatedConfig: 'Generated config', generatedConfig: 'Generated config',
serviceLabel: 'Service', serviceLabel: 'Service',
@@ -207,6 +208,7 @@ const text = {
deleteAction: 'Удалить пользователя', deleteAction: 'Удалить пользователя',
}, },
settings: { settings: {
panelTitle: 'Настройки панели',
title: 'Сервисы', title: 'Сервисы',
generatedConfig: 'Сгенерированный конфиг', generatedConfig: 'Сгенерированный конфиг',
serviceLabel: 'Сервис', serviceLabel: 'Сервис',