feature/ignore-self-toggle #1
@@ -4,7 +4,7 @@ This document breaks the entire BanGUI project into development stages, ordered
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Config View Redesign — List/Detail Layout with Active Status and Raw Export
|
## Config View Redesign — List/Detail Layout with Active Status and Raw Export ✅ COMPLETE
|
||||||
|
|
||||||
### Overview
|
### Overview
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,13 @@ export async function fetchJailConfigFileContent(
|
|||||||
return get<JailConfigFileContent>(ENDPOINTS.configJailFile(filename));
|
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(
|
export async function setJailConfigFileEnabled(
|
||||||
filename: string,
|
filename: string,
|
||||||
update: JailConfigFileEnabledUpdate
|
update: JailConfigFileEnabledUpdate
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const {
|
|||||||
mockUpdateServerSettings,
|
mockUpdateServerSettings,
|
||||||
mockFlushLogs,
|
mockFlushLogs,
|
||||||
mockSetJailConfigFileEnabled,
|
mockSetJailConfigFileEnabled,
|
||||||
|
mockUpdateJailConfigFile,
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
mockAddLogPath: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
mockAddLogPath: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
||||||
mockDeleteLogPath: 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),
|
mockUpdateServerSettings: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
||||||
mockFlushLogs: vi.fn().mockResolvedValue({ message: "ok" }),
|
mockFlushLogs: vi.fn().mockResolvedValue({ message: "ok" }),
|
||||||
mockSetJailConfigFileEnabled: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
mockSetJailConfigFileEnabled: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
||||||
|
mockUpdateJailConfigFile: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../api/config", () => ({
|
vi.mock("../../api/config", () => ({
|
||||||
@@ -91,6 +93,7 @@ vi.mock("../../api/config", () => ({
|
|||||||
updateMapColorThresholds: mockUpdateMapColorThresholds,
|
updateMapColorThresholds: mockUpdateMapColorThresholds,
|
||||||
fetchJailConfigFiles: mockFetchJailConfigFiles,
|
fetchJailConfigFiles: mockFetchJailConfigFiles,
|
||||||
fetchJailConfigFileContent: vi.fn(),
|
fetchJailConfigFileContent: vi.fn(),
|
||||||
|
updateJailConfigFile: mockUpdateJailConfigFile,
|
||||||
setJailConfigFileEnabled: mockSetJailConfigFileEnabled,
|
setJailConfigFileEnabled: mockSetJailConfigFileEnabled,
|
||||||
fetchFilterFiles: mockFetchFilterFiles,
|
fetchFilterFiles: mockFetchFilterFiles,
|
||||||
fetchFilterFile: vi.fn(),
|
fetchFilterFile: vi.fn(),
|
||||||
@@ -104,6 +107,10 @@ vi.mock("../../api/config", () => ({
|
|||||||
testRegex: vi.fn(),
|
testRegex: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../api/jails", () => ({
|
||||||
|
fetchJails: vi.fn().mockResolvedValue({ jails: [], total: 0 }),
|
||||||
|
}));
|
||||||
|
|
||||||
/** Minimal jail fixture used across tests. */
|
/** Minimal jail fixture used across tests. */
|
||||||
const MOCK_JAIL: JailConfig = {
|
const MOCK_JAIL: JailConfig = {
|
||||||
name: "sshd",
|
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>) {
|
async function openSshdAccordion(user: ReturnType<typeof userEvent.setup>) {
|
||||||
const accordionBtn = await screen.findByRole("button", { name: /sshd/i });
|
const listItem = await screen.findByRole("option", { name: /sshd/i });
|
||||||
await user.click(accordionBtn);
|
await user.click(listItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -12,6 +12,71 @@ export const useConfigStyles = makeStyles({
|
|||||||
padding: tokens.spacingVerticalXXL,
|
padding: tokens.spacingVerticalXXL,
|
||||||
maxWidth: "1100px",
|
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: {
|
header: {
|
||||||
marginBottom: tokens.spacingVerticalL,
|
marginBottom: tokens.spacingVerticalL,
|
||||||
},
|
},
|
||||||
@@ -176,6 +241,11 @@ export function injectGlobalStyles(): void {
|
|||||||
from { opacity: 0; transform: scale(0.95); }
|
from { opacity: 0; transform: scale(0.95); }
|
||||||
to { opacity: 1; transform: scale(1); }
|
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);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export { AutoSaveIndicator } from "./AutoSaveIndicator";
|
|||||||
export type { AutoSaveStatus, AutoSaveIndicatorProps } from "./AutoSaveIndicator";
|
export type { AutoSaveStatus, AutoSaveIndicatorProps } from "./AutoSaveIndicator";
|
||||||
export { ConfFilesTab } from "./ConfFilesTab";
|
export { ConfFilesTab } from "./ConfFilesTab";
|
||||||
export type { ConfFilesTabProps } from "./ConfFilesTab";
|
export type { ConfFilesTabProps } from "./ConfFilesTab";
|
||||||
|
export { ConfigListDetail } from "./ConfigListDetail";
|
||||||
|
export type { ConfigListDetailProps } from "./ConfigListDetail";
|
||||||
export { ExportTab } from "./ExportTab";
|
export { ExportTab } from "./ExportTab";
|
||||||
export { FilterForm } from "./FilterForm";
|
export { FilterForm } from "./FilterForm";
|
||||||
export type { FilterFormProps } from "./FilterForm";
|
export type { FilterFormProps } from "./FilterForm";
|
||||||
@@ -21,6 +23,8 @@ export { JailFilesTab } from "./JailFilesTab";
|
|||||||
export { JailFileForm } from "./JailFileForm";
|
export { JailFileForm } from "./JailFileForm";
|
||||||
export { JailsTab } from "./JailsTab";
|
export { JailsTab } from "./JailsTab";
|
||||||
export { MapTab } from "./MapTab";
|
export { MapTab } from "./MapTab";
|
||||||
|
export { RawConfigSection } from "./RawConfigSection";
|
||||||
|
export type { RawConfigSectionProps } from "./RawConfigSection";
|
||||||
export { RegexList } from "./RegexList";
|
export { RegexList } from "./RegexList";
|
||||||
export type { RegexListProps } from "./RegexList";
|
export type { RegexListProps } from "./RegexList";
|
||||||
export { RegexTesterTab } from "./RegexTesterTab";
|
export { RegexTesterTab } from "./RegexTesterTab";
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ class ResizeObserverStub {
|
|||||||
|
|
||||||
globalThis.ResizeObserver = ResizeObserverStub;
|
globalThis.ResizeObserver = ResizeObserverStub;
|
||||||
|
|
||||||
|
// jsdom does not implement scrollIntoView.
|
||||||
|
Element.prototype.scrollIntoView = () => {};
|
||||||
|
|
||||||
// Fluent UI animations rely on matchMedia.
|
// Fluent UI animations rely on matchMedia.
|
||||||
Object.defineProperty(window, "matchMedia", {
|
Object.defineProperty(window, "matchMedia", {
|
||||||
writable: true,
|
writable: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user