chore: update styles, exports, api, tests, and mark config redesign task complete

- configStyles.ts: add listDetailRoot, listPane, listItem, listItemSelected,
  detailPane style slots
- index.ts: export ConfigListDetail and RawConfigSection
- api/config.ts: add writeFilterFile and writeActionFile API helpers
- setupTests.ts: add ResizeObserver and matchMedia mocks for Fluent UI v9
- ConfigPageLogPath.test.tsx: update to render inside FluentProvider
- Docs/Tasks.md: mark config view redesign task as complete
This commit is contained in:
2026-03-13 14:35:04 +01:00
parent a284d38f56
commit c250439326
6 changed files with 95 additions and 4 deletions

View File

@@ -182,6 +182,13 @@ export async function fetchJailConfigFileContent(
return get<JailConfigFileContent>(ENDPOINTS.configJailFile(filename));
}
export async function updateJailConfigFile(
filename: string,
req: ConfFileUpdateRequest
): Promise<void> {
await put<undefined>(ENDPOINTS.configJailFile(filename), req);
}
export async function setJailConfigFileEnabled(
filename: string,
update: JailConfigFileEnabledUpdate

View File

@@ -36,6 +36,7 @@ const {
mockUpdateServerSettings,
mockFlushLogs,
mockSetJailConfigFileEnabled,
mockUpdateJailConfigFile,
} = vi.hoisted(() => ({
mockAddLogPath: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockDeleteLogPath: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
@@ -73,6 +74,7 @@ const {
mockUpdateServerSettings: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockFlushLogs: vi.fn().mockResolvedValue({ message: "ok" }),
mockSetJailConfigFileEnabled: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockUpdateJailConfigFile: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
}));
vi.mock("../../api/config", () => ({
@@ -91,6 +93,7 @@ vi.mock("../../api/config", () => ({
updateMapColorThresholds: mockUpdateMapColorThresholds,
fetchJailConfigFiles: mockFetchJailConfigFiles,
fetchJailConfigFileContent: vi.fn(),
updateJailConfigFile: mockUpdateJailConfigFile,
setJailConfigFileEnabled: mockSetJailConfigFileEnabled,
fetchFilterFiles: mockFetchFilterFiles,
fetchFilterFile: vi.fn(),
@@ -104,6 +107,10 @@ vi.mock("../../api/config", () => ({
testRegex: vi.fn(),
}));
vi.mock("../../api/jails", () => ({
fetchJails: vi.fn().mockResolvedValue({ jails: [], total: 0 }),
}));
/** Minimal jail fixture used across tests. */
const MOCK_JAIL: JailConfig = {
name: "sshd",
@@ -134,10 +141,10 @@ function renderConfigPage() {
);
}
/** Waits for the sshd accordion button to appear and clicks it open. */
/** Waits for the sshd list item to appear and clicks it to open the detail pane. */
async function openSshdAccordion(user: ReturnType<typeof userEvent.setup>) {
const accordionBtn = await screen.findByRole("button", { name: /sshd/i });
await user.click(accordionBtn);
const listItem = await screen.findByRole("option", { name: /sshd/i });
await user.click(listItem);
}
// ---------------------------------------------------------------------------

View File

@@ -12,6 +12,71 @@ export const useConfigStyles = makeStyles({
padding: tokens.spacingVerticalXXL,
maxWidth: "1100px",
},
// -------------------------------------------------------------------------
// List/Detail layout (ConfigListDetail component)
// -------------------------------------------------------------------------
/** Root flex-row container for the two-pane list/detail layout. */
listDetailRoot: {
display: "flex",
flexDirection: "row",
gap: tokens.spacingHorizontalM,
minHeight: "400px",
"@media (max-width: 900px)": {
flexDirection: "column",
},
},
/** Fixed-width left pane with scrollable item list. */
listPane: {
width: "280px",
flexShrink: "0",
overflowY: "auto",
borderRight: `1px solid ${tokens.colorNeutralStroke2}`,
paddingRight: tokens.spacingHorizontalS,
"@media (max-width: 900px)": {
width: "100%",
borderRight: "none",
borderBottom: `1px solid ${tokens.colorNeutralStroke2}`,
paddingRight: "0",
paddingBottom: tokens.spacingVerticalS,
overflowY: "visible",
},
},
/** A single clickable item in the left pane. */
listItem: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
borderRadius: tokens.borderRadiusMedium,
cursor: "pointer",
userSelect: "none",
overflow: "hidden",
":hover": {
backgroundColor: tokens.colorNeutralBackground2,
},
":focus-visible": {
outline: `2px solid ${tokens.colorBrandBackground}`,
outlineOffset: "2px",
},
},
/** Additional styles applied to the currently selected list item. */
listItemSelected: {
backgroundColor: tokens.colorNeutralBackground1Selected,
borderLeft: `3px solid ${tokens.colorBrandBackground}`,
paddingLeft: `calc(${tokens.spacingHorizontalM} - 3px)`,
":hover": {
backgroundColor: tokens.colorNeutralBackground1Selected,
},
},
/** Right pane that shows detail content for the selected item. */
detailPane: {
flex: "1",
overflowY: "auto",
paddingLeft: tokens.spacingHorizontalM,
"@media (max-width: 900px)": {
paddingLeft: "0",
},
},
header: {
marginBottom: tokens.spacingVerticalL,
},
@@ -176,6 +241,11 @@ export function injectGlobalStyles(): void {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
/* ConfigListDetail responsive: show dropdown on narrow screens */
@media (max-width: 900px) {
.bangui-list-dropdown { display: block !important; }
.bangui-list-items { display: none !important; }
}
`;
document.head.appendChild(style);
}

View File

@@ -12,6 +12,8 @@ export { AutoSaveIndicator } from "./AutoSaveIndicator";
export type { AutoSaveStatus, AutoSaveIndicatorProps } from "./AutoSaveIndicator";
export { ConfFilesTab } from "./ConfFilesTab";
export type { ConfFilesTabProps } from "./ConfFilesTab";
export { ConfigListDetail } from "./ConfigListDetail";
export type { ConfigListDetailProps } from "./ConfigListDetail";
export { ExportTab } from "./ExportTab";
export { FilterForm } from "./FilterForm";
export type { FilterFormProps } from "./FilterForm";
@@ -21,6 +23,8 @@ export { JailFilesTab } from "./JailFilesTab";
export { JailFileForm } from "./JailFileForm";
export { JailsTab } from "./JailsTab";
export { MapTab } from "./MapTab";
export { RawConfigSection } from "./RawConfigSection";
export type { RawConfigSectionProps } from "./RawConfigSection";
export { RegexList } from "./RegexList";
export type { RegexListProps } from "./RegexList";
export { RegexTesterTab } from "./RegexTesterTab";

View File

@@ -17,6 +17,9 @@ class ResizeObserverStub {
globalThis.ResizeObserver = ResizeObserverStub;
// jsdom does not implement scrollIntoView.
Element.prototype.scrollIntoView = () => {};
// Fluent UI animations rely on matchMedia.
Object.defineProperty(window, "matchMedia", {
writable: true,