Skip to content

Conversation

@arikorn
Copy link
Contributor

@arikorn arikorn commented Jan 20, 2026

Consolidate settings and integrations with the configured-surfaces page

Previously, the information needed to diagnose surface connection problems was spread across two pages: configured surfaces and settings/surfaces. In 4.3, the with the introduction of the Integrations page, information is spread across three pages. This PR rearranges and consolidates all of this information into a single page, taking advantage of the blank default right-panel on the configured-surfaces page.

(NB: This PR is the continuation of a conversation in PR #3703 that we agreed to defer until after that one was merged, so a purpose of this PR is to give a concrete visualization of my ideas discussed there.)

There's quite a bit of detail here. It may be best to start by watching the video, below.

Features:

  • The layout is similar to the Connections page:
  • When nothing is selected the right-panel shows the settings and instance table -- making it easy to disable/enable an integration or setting and to confirm their state.
  • Clicking on any configured surface/group brings up the edit panel as usual, but with the "X" close-icon visible.
  • Clicking on the selected surface or the "X" icon closes the edit panel/re-reveals the settings
  • Clicking on an item in the instances table (right panel) brings up the instance configuration in that panel (with the "X" close-icon, of course).
  • Click on "Add Integration" above the instances table to add a new instance directly from this page.
  • When screen is narrow, an "open settings panel" button appears.
Companion.Surface.Demo.reduced.mp4

General Notes:

  • For the most part this PR repurposes existing code, with some changes to visibility, rather than adding new Components, so although it affects many files, the changes to most are minor.
  • The current implementation allows the Configured Surfaces page to coexist with the Integrations page, but that page is no longer necessary. Since the Integrations page has never been "released in the wild", I would recommend replacing it with this PR (I can add that to the code.) Note that removing the Integrations page as a separate entity could simplify the conditional coding included here.
  • Likewise, the settings/surfaces page would not be necessary but one might want to preserve it for historical reasons (though note that most of its functionality has already been move or removed in 4.3 beta).
  • (The inspector in Chrome insists I use thead/tbody to tables or else it emits dozens of DOM errors.)

Formatting notes (all of which should be taken as suggestions):

  • Put the integrations table in a table-looking box, make the instructions more consise, fiddle with spacing.
  • The original instructions for integrations are in the hover-help.
  • Rearrange settings and improve formatting slightly.
  • In particular "Enable Elgato software Plugin" is now grouped with the instance table, since it serves a similar function from the user POV (enabling a class of inputs).
  • add "close" and "help" icons for the panel headers, visually formatted for enhanced visibility.
  • add context-help icons to the table-headers inside the right panel.

The best way to think about the benefits of this PR is in terms of workflows:
A. Adding new integrations will always be rare relative to configuring/diagnosing surfaces, so for the most part the "root" right-panel is giving you instant enable/disabled status, as well as showing when it's time to update an instance all in the one page. Consider when a surface isn't showing up: is it disabled? is the integration-instance loaded? is the integration disabled? does it need to be updated? All of this information is now immediately seen in one page.

B. Here's how I envision a typical add-integration workflow: 1. plug in your surface 2. nothing shows up in the left panel. 3. look to the right, is the module enabled? 4. oh, it's not installed yet! click on "Add Surface Integration" and add it. 5. now everything's working and you didn't have to find or visit other pages: all the information you need is right there (and taking up what used to be blank space).

Notice that the interaction starts by looking at the left panel and is all done with the goal of getting something to show up in the left panel, i.e., it starts and ends with the configured-surfaces list (without having to flip back and forth between pages), so the new right panel fits naturally on this page.

C. An alternate add-integration workflow would be the person "knowing" that they need to add a new brand of surface and now that they've seen the "Add Surface Integrations" button (and list of installed integrations) every time they were on the surfaces page, they know exactly where to go.. (so maybe an extra click or maybe saves four clicks if they start at the settings page and then have to go back to the integrations page...)

The video illustrates these workflows -- in the first part of the video my surface didn't connect because the module was missing, so I add the module. In the last part the module was disabled, which is instantly apparent. The middle part of the video shows how the layout changes with screen-width changes (and appearance of the "open settings" button).

Commit notes:
The PR is split into several commits because some of the changes here can be applied more generally. The commits are:

  1. The main change
  2. Fixing some other pages affected by a CSS improvement.
  3. Add the "show settings" button, which could be ported to the Connections page, for example
  4. Change "Cancel" to "Done" when surface instance properties are not dirty. This is alot less confusing to see.
  5. Add option hover-help via a CircleQuestion icon in user-config headers (table headers, not the panel)
  6. Add the enhanced "Close" and "Help" buttons for the top-right of panels.
  7. Fix a formatting bug introduced somewhere in 4.3 beta -- the bug does not originate from this PR.

Comment: All settings pages would probably benefit from rewriting as Containers with rows & columns instead of tables...

Summary by CodeRabbit

  • New Features

    • New comprehensive Surface Settings panel with global options and an integrations area.
    • Add/Edit integration subpanels for managing surface integrations.
  • UI/UX Improvements

    • Replaced icon-only controls with reusable close and help buttons for consistent behavior.
    • Contextual help and tooltips added to headings.
    • Cancel button now shows "Done" when no changes are pending.
    • Various layout and styling adjustments for responsive panels and forms.

✏️ Tip: You can customize this high-level summary in your review settings.

- Behavior is similar to the layout on the Connections page:
- When nothing is selected the right-panel shows the settings and instance table -- making it easy to disable/enable an integration (and to confirm its state).
- Click on any configured surface/group brings up the edit panel as usual, but with the "X" close-icon always visible.
- Click on the selected surface or the "X" icon closes the edit panel/reveals the settings
- Click on an item in the instances table (right panel) brings up the instance configuration panel in that panel (with the "X" close-icon, of course).
- Click on "Add Integration" above the instances table to add a new instance (instead of having to navigate to the other page)

The current implementation allows the Configured Surfaces page to coexist with the Integrations page, but that page is no longer necessary.
Removing the Integrations page could simplify the conditional coding included here.

Notes:
- (The inspector in Chrome insists I use thead/tbody or else it emits dozens of DOM errors.)
- For the most part this PR repurposes existing code, with some changes to visibility, rather than adding new Components.

Formatting (all of which should be taken as suggestions)
- Put the integrations table in a table-looking box, make the instructions more consise, fiddle with spacing.
- Rearrange switches and improve formatting slightly.
- In particular "Enable Elgato software Plugin" is now grouped with the instance table, since it serves a similar function (enabling a class of inputs).
Setting width=100% for the first column in UserConfigSwitchRow squeezes text and numeric entries in tables that include switches and text/number input. This commit fixes the affected settings pages.
When the window is too narrow, a new button is presented to show the settings panel.
This avoids confusion, especially when the panel is opened automatically upon addition of an instance.
- optional `tooltip` property for `UserConfigHeadingRow`
- shows the help circle at the right of the heading row.
Add a reusable, style-able panel-close button

- Make it very visible, so user see how to "remove" a panel (the plain X is fine one you see it but...)
- Generalize the button so it can provide a consistent look across pages
- Give it a CSS class so we can fiddle with details
- Apply it to the four closeable Surface panels

Add a context-help button with similar styling to the panel-close button. It allows both hover help and going to a user-guide page or running a custom function.
In the current beta (not just this PR), the configured surfaces list grows when window-height is greater than a threshold. But this only happens when no row is selected, resulting in the row height changing when you select an item. The cause is partly because the parent container `CCol` just inside `ConfiguredSuracesPage` acquiring class `d-xl-block`, which overrides flex behavior.

Removing `flex-grow: 1` from `.scrollable-container` in _sticky-tables.scss fixes it by always suppressing table-height growth. Personally, I don't see any benefit in stretching the row height. No other page/panel appears to be affected by this change.
@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

Introduces reusable CloseButton and ContextHelpButton components, adds a ConfiguredSurfaceContext and new configured/integration routes, updates panels and lists to use dynamic routing and new props (e.g., isSubpanel, toDir), and adjusts layout/styles for responsive settings UIs.

Changes

Cohort / File(s) Change Summary
Common UI components
webui/src/UserConfig/Components/Common.tsx
Added public CloseButton and ContextHelpButton components and their prop interfaces; new icon imports and styling hooks.
Heading / Switch rows
webui/src/UserConfig/Components/UserConfigHeadingRow.tsx, webui/src/UserConfig/Components/UserConfigSwitchRow.tsx
UserConfigHeadingRow gains optional tooltip?: string prop and renders an InlineHelp icon; UserConfigSwitchRow adjusts table cell widths/classes for layout.
Instances panels
webui/src/Instances/AddInstancePanel.tsx, webui/src/Instances/InstanceEdit/InstanceEditPanel.tsx
AddInstancePanel adds optional isSubpanel?: boolean prop and uses CloseButton; InstanceEdit cancel label becomes dynamic ("Cancel" vs "Done") based on dirty state.
Surfaces panels & headings
webui/src/Surfaces/EditPanel.tsx, webui/src/Surfaces/Instances/AddSurfaceInstancePanel.tsx, webui/src/Surfaces/Instances/SurfaceInstanceEdit/SurfaceInstanceEditPanel.tsx, webui/src/Surfaces/Instances/SurfaceInstanceEdit/SurfaceInstanceEditPanelHeading.tsx
Replaced inline faTimes/faQuestion icons with CloseButton and ContextHelpButton; several panels now accept/forward isSubpanel; navigation targets become computed from useLocation (dynamic homepage/instance paths); SurfaceInstanceEditPanelHeading adds closeConfigurePanel: () => void prop.
Configured surfaces & context
webui/src/Surfaces/ConfiguredSurfacesContext.tsx, webui/src/Surfaces/ConfiguredSurfacesPage.tsx
New ConfiguredSurfaceContext export and useConfiguredSurfaceContext hook; ConfiguredSurfacesPage adds showSettings state, wraps Outlet in provider, and exposes settings toggle.
Surface instances list
webui/src/Surfaces/Instances/SurfaceInstanceList/SurfaceInstanceList.tsx
SurfaceInstancesList adds optional toDir?: string prop and uses dynamic homepage routing; header/description conditional on toDir.
Routes (configured/integration)
webui/src/routeTree.gen.ts, webui/src/routes/app/surfaces/configured/integration.add.tsx, webui/src/routes/app/surfaces/configured/integration.$instanceId.tsx
Added nested routes for /surfaces/configured/integration/add and /surfaces/configured/integration/$instanceId; updated generated route typings and new route components that render AddSurfaceInstancePanel (isSubpanel) and SurfaceInstanceEditPanel.
Main configured surfaces page
webui/src/routes/app/surfaces/configured/index.tsx
Replaced previous route component with SurfaceSettingsPanel, introducing a settings-driven panel UI, help and close buttons wired to the new context, and integrations listing.
User config grouping & small UI changes
webui/src/UserConfig/general.tsx, webui/src/UserConfig/surfaces.tsx, webui/src/UserConfig/Sections/DataCollection.tsx, webui/src/UserConfig/Sections/PinLockoutConfig.tsx
Separated tables for config sections, exported UserConfigTable, added tooltip to PIN Lockout heading, and added uc-button class to "View Data" button for styling.
Styling updates
webui/src/scss/_layout.scss, webui/src/scss/_settings.scss, webui/src/scss/_sticky-tables.scss
Added .panel-close-button and .context-help-button-2xl styles; set min-width for inputs and .uc-button in settings tables; relaxed .scrollable-container/.flex-column-layout flex constraints.

Poem

Little buttons, context bright,
Panels find their proper light ✨
Routes sprout paths both near and far,
Headings wink with helpful star ⭐
Layout breathes — a UI cheer! 🎉

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the PR: consolidating surface settings into a single, unified location within the Configured Surfaces page.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
webui/src/Surfaces/Instances/SurfaceInstanceEdit/SurfaceInstanceEditPanel.tsx (1)

70-77: Smart dynamic navigation - nice refactor!

Great idea deriving the homepage from the current pathname rather than hardcoding. This makes the component more reusable across different route structures.

One small thought: the pathname.split('/', 3).join('/') approach works perfectly for paths like /surfaces/configured/..., but if this component ever ends up on a shorter path, it might produce unexpected results. Given the route structure though, this seems unlikely to be an issue in practice.

If you wanted extra safety, you could add a fallback:

💡 Optional defensive fallback
-	const homepage = pathname.split('/', 3).join('/') // first two elements of the path ex: /surfaces/configured
+	const homepage = pathname.split('/', 3).join('/') || '/surfaces/configured' // fallback if path is unexpectedly short
webui/src/Surfaces/ConfiguredSurfacesContext.tsx (1)

8-18: Nice context setup! One small note on the validation logic.

Thanks for adding the lint comment explaining the file structure - that's helpful context!

One thing to note: since createContext on line 6 provides a default value, the if (!context) check on lines 10-12 will never trigger - the context will always be at least the default object. This check would only be useful if you passed null or undefined as the default.

The setShowSettings validation on lines 13-17 is more useful since it could catch malformed provider values, though with TypeScript the type system should help prevent that.

If you'd like to catch "used outside provider" scenarios, you could use null as the default:

💡 Optional pattern for detecting missing provider
-export const ConfiguredSurfaceContext = createContext({ setShowSettings: (_a: boolean) => {} })
+export const ConfiguredSurfaceContext = createContext<{ setShowSettings: (a: boolean) => void } | null>(null)

 export const useConfiguredSurfaceContext = (): booleanFn => {
 	const context = useContext(ConfiguredSurfaceContext)
 	if (!context) {
 		throw new Error('useConfiguredSurfaceContext must be used within a ConfiguredSurfaceContext provider')
 	}
-	if (!context.setShowSettings || typeof context.setShowSettings !== 'function') {
-		throw new Error(
-			'ConfiguredSurfaceContext provider must be an object with a setShowSettings property set to a function'
-		)
-	}
 	return context.setShowSettings
 }

Either approach works - the current code is perfectly functional, this is just a pattern suggestion for stricter provider enforcement if you want it!

webui/src/UserConfig/Components/UserConfigHeadingRow.tsx (1)

18-24: Consider reusing ContextHelpButton for consistency (optional)

This works well! Just a small thought: the project now has a ContextHelpButton in Common.tsx that also wraps FontAwesomeIcon + InlineHelp. If you wanted to unify the help icon styling across the app, you could potentially use it here too (without the userGuide prop, it would just show the hover text).

That said, this simpler inline approach is totally fine if the styling needs differ here. No action needed if you prefer keeping it as-is!

webui/src/Surfaces/Instances/AddSurfaceInstancePanel.tsx (1)

63-72: Path computation works but is a bit fragile

I appreciate the honest comment about this not being pretty! 😄 The logic works for the current route structure, but a few edge cases to keep in mind:

  1. pathname.split('/', 3).join('/') assumes at least 2 path segments exist
  2. replace('add', '$instanceId') does a simple string replace - if add ever appears elsewhere in a path segment, this could misbehave

For now this seems fine given the controlled route structure. If you wanted to make it more robust later, you could use pathname.lastIndexOf('/') to safely extract the parent path, or leverage TanStack Router's path building utilities.

No immediate action needed - just something to revisit if the routing gets more complex!

webui/src/routes/app/surfaces/configured/index.tsx (1)

4-4: Consider removing commented-out import

This commented-out import might be a leftover from refactoring. If it's no longer needed, feel free to remove it when you get a chance to keep the file tidy. No rush on this one! 🧹

Comment on lines +19 to +25
useComputed(() => {
if (!surfaceInstances.instances.has(instanceId)) {
void navigate({ to: '/surfaces/configured' })
}
}, [navigate, surfaceInstances, instanceId])

return <SurfaceInstanceEditPanel key={instanceId} instanceId={instanceId} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Navigation during render may cause issues

The validation logic looks correct, but calling navigate() inside useComputed (which runs during render) could lead to React warnings or unexpected behavior. The component also continues to render SurfaceInstanceEditPanel even when the instance is invalid and navigation is triggered.

A safer pattern would be to either:

  1. Return early with null when the instance is invalid, or
  2. Move the navigation to a useEffect
💡 Suggested approach
 function ModuleConfigComponent() {
 	const { instanceId } = Route.useParams()
 
 	const { surfaceInstances } = useContext(RootAppStoreContext)
 
 	const navigate = useNavigate()
+	const instanceExists = surfaceInstances.instances.has(instanceId)
 
-	// Ensure the selected instance is valid
-	useComputed(() => {
-		if (!surfaceInstances.instances.has(instanceId)) {
+	// Redirect if instance doesn't exist
+	React.useEffect(() => {
+		if (!instanceExists) {
 			void navigate({ to: '/surfaces/configured' })
 		}
-	}, [navigate, surfaceInstances, instanceId])
+	}, [navigate, instanceExists])
+
+	if (!instanceExists) {
+		return null
+	}
 
 	return <SurfaceInstanceEditPanel key={instanceId} instanceId={instanceId} />
 }

Comment on lines +111 to 118
<div className="connection-group-actions mb-2 mx-1">
<CButtonGroup>
<CButton
color="primary"
size="sm"
className="d-xl-none"
onClick={() => void navigate({ to: '/surfaces/integrations/add' })}
className={toDir ? 'undefined' : 'd-xl-none'}
onClick={() => void navigate({ to: `/surfaces/${toDir ?? 'integrations'}/add` })}
>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid literal "undefined" class on the Add button.
This currently injects a real CSS class named undefined when toDir is set.

🛠️ Proposed fix
-							className={toDir ? 'undefined' : 'd-xl-none'}
+							className={toDir ? undefined : 'd-xl-none'}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="connection-group-actions mb-2 mx-1">
<CButtonGroup>
<CButton
color="primary"
size="sm"
className="d-xl-none"
onClick={() => void navigate({ to: '/surfaces/integrations/add' })}
className={toDir ? 'undefined' : 'd-xl-none'}
onClick={() => void navigate({ to: `/surfaces/${toDir ?? 'integrations'}/add` })}
>
<div className="connection-group-actions mb-2 mx-1">
<CButtonGroup>
<CButton
color="primary"
size="sm"
className={toDir ? undefined : 'd-xl-none'}
onClick={() => void navigate({ to: `/surfaces/${toDir ?? 'integrations'}/add` })}
>

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
webui/src/routes/app/surfaces/configured/index.tsx (3)

4-4: Consider removing the commented-out import.

This commented import for SurfacesConfig appears to be leftover from refactoring. Feel free to remove it when you get a chance to keep things tidy! 🧹


67-77: Prefer CSS for bold styling instead of wrapping the table in <b>.

Wrapping a <table> inside a <b> tag is semantically unusual - <b> is intended for inline phrasing content. While it might render as expected in most browsers, using CSS would be cleaner and more predictable.

✨ Suggested approach
-				{userConfigProps && (
-					<b>
-						<table className="table table-responsive-sm table-settings ms-3">
-							<tbody>
-								<UserConfigSwitchRow
-									userConfig={userConfigProps}
-									label="Enable Elgato software Plugin support"
-									field="elgato_plugin_enable"
-								/>
-							</tbody>
-						</table>
-					</b>
-				)}
+				{userConfigProps && (
+					<table className="table table-responsive-sm table-settings ms-3" style={{ fontWeight: 'bold' }}>
+						<tbody>
+							<UserConfigSwitchRow
+								userConfig={userConfigProps}
+								label="Enable Elgato software Plugin support"
+								field="elgato_plugin_enable"
+							/>
+						</tbody>
+					</table>
+				)}

Alternatively, if you'd like to keep styles in CSS, you could add a class for this.


24-29: Optional: Consider extracting repeated inline styles to CSS classes.

There are several inline styles throughout the component (padding, margins, borders). While they work fine, extracting them to CSS classes could make future tweaks easier. Totally optional and can be a follow-up if you prefer - no blocker here!


function SettingsPanelTitleBar() {
const setPanelVisibility = useConfiguredSurfaceContext()
// note that the close button is currently hidden
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The comment might be slightly misleading.

The comment says "close button is currently hidden," but looking at line 110, the button uses visibilityClass="d-xl-none" which means it's actually visible on screens smaller than XL (and hidden only on XL+). If this responsive behavior is intentional, perhaps updating the comment would help future readers understand the design decision?

📝 Suggested comment update
-	// note that the close button is currently hidden
+	// note: close button is hidden on XL+ screens (settings panel is always visible there)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// note that the close button is currently hidden
// note: close button is hidden on XL+ screens (settings panel is always visible there)

@arikorn
Copy link
Contributor Author

arikorn commented Jan 20, 2026

(Note: I'll wait on comments/approval before dancing with the Rabbit.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant