Separate panel settings from services
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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: 'Сервис',
|
||||||
|
|||||||
Reference in New Issue
Block a user