feat(FN-2584): gate remote access settings behind experimental flag
- Register remoteAccess in KNOWN_EXPERIMENTAL_FEATURES so it appears in Experimental Features toggles - Compute visible settings sections from experimental flags and hide Remote Access nav/select entries when disabled - Redirect stale initial or active remote section selection to the first visible section instead of rendering a hidden section - Add SettingsModal tests covering remote section visibility, experimental toggle presence, and disabled-state fallback behavior
This commit is contained in:
@@ -101,6 +101,7 @@ const KNOWN_EXPERIMENTAL_FEATURES: Record<string, string> = {
|
||||
insights: "Insights",
|
||||
roadmap: "Roadmaps",
|
||||
memoryView: "Memory Editor",
|
||||
remoteAccess: "Remote Access",
|
||||
skillsView: "Skills View",
|
||||
nodesView: "Nodes View",
|
||||
devServerView: "Dev Server",
|
||||
@@ -230,8 +231,18 @@ export function SettingsModal({
|
||||
refresh: refreshOverlapPathPicker,
|
||||
} = useWorkspaceFileBrowser("project", overlapPathPickerIndex !== null, projectId);
|
||||
|
||||
const remoteAccessEnabled = isExperimentalFeatureEnabled(form.experimentalFeatures ?? {}, "remoteAccess");
|
||||
const visibleSections = SETTINGS_SECTIONS.filter((section) => section.id !== "remote" || remoteAccessEnabled);
|
||||
const firstVisibleSectionId = visibleSections.find((section) => !section.isGroupHeader)?.id ?? "general";
|
||||
|
||||
/** Get the scope of the currently active section */
|
||||
const activeSectionScope = SETTINGS_SECTIONS.find((s) => s.id === activeSection)?.scope;
|
||||
const activeSectionScope = visibleSections.find((s) => s.id === activeSection)?.scope;
|
||||
|
||||
useEffect(() => {
|
||||
if (activeSection === "remote" && !remoteAccessEnabled) {
|
||||
setActiveSection(firstVisibleSectionId);
|
||||
}
|
||||
}, [activeSection, remoteAccessEnabled, firstVisibleSectionId]);
|
||||
|
||||
// Auth state (independent of the settings save flow)
|
||||
const [authProviders, setAuthProviders] = useState<AuthProvider[]>([]);
|
||||
@@ -4023,7 +4034,7 @@ export function SettingsModal({
|
||||
value={activeSection}
|
||||
onChange={(event) => setActiveSection(event.target.value as SectionId)}
|
||||
>
|
||||
{SETTINGS_SECTIONS.filter((section) => !section.isGroupHeader).map((section) => (
|
||||
{visibleSections.filter((section) => !section.isGroupHeader).map((section) => (
|
||||
<option key={section.id} value={section.id}>
|
||||
{section.label}
|
||||
</option>
|
||||
@@ -4032,7 +4043,7 @@ export function SettingsModal({
|
||||
</div>
|
||||
)}
|
||||
<nav className="settings-sidebar">
|
||||
{SETTINGS_SECTIONS.map((section) => {
|
||||
{visibleSections.map((section) => {
|
||||
// Render group headers as non-clickable styled divs
|
||||
if (section.isGroupHeader) {
|
||||
return (
|
||||
|
||||
@@ -1138,6 +1138,57 @@ describe("SettingsModal", () => {
|
||||
expect(devServerToggles[0]).toBeChecked();
|
||||
});
|
||||
|
||||
describe("Remote Access section visibility", () => {
|
||||
it("hides Remote Access nav item when experimentalFeatures.remoteAccess is falsy", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {},
|
||||
});
|
||||
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
expect(screen.queryByRole("button", { name: /Remote Access/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Remote Access nav item when experimentalFeatures.remoteAccess is true", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { remoteAccess: true },
|
||||
});
|
||||
|
||||
renderModal();
|
||||
|
||||
expect(await screen.findByRole("button", { name: /Remote Access/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Remote Access in KNOWN_EXPERIMENTAL_FEATURES toggle list", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {},
|
||||
});
|
||||
|
||||
renderModal();
|
||||
|
||||
await openExperimentalFeaturesSection();
|
||||
|
||||
expect(screen.getByLabelText("Remote Access")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("falls back to the first selectable section when opening remote while remoteAccess is disabled", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {},
|
||||
});
|
||||
|
||||
renderModal({ initialSection: "remote" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
expect(screen.queryByRole("button", { name: /Remote Access/i })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: "Authentication" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes legacy devServer flag to canonical devServerView on save", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
@@ -1299,6 +1350,13 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
|
||||
describe("Remote section", () => {
|
||||
beforeEach(() => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { remoteAccess: true },
|
||||
});
|
||||
});
|
||||
|
||||
const openRemoteSection = async () => {
|
||||
const [remoteSectionButton] = await screen.findAllByRole("button", { name: /Remote Access/i });
|
||||
await userEvent.click(remoteSectionButton);
|
||||
|
||||
Reference in New Issue
Block a user