Skip to content

feat: make ConnectionFormView fully dynamic via plugin metadata#309

Merged
datlechin merged 4 commits intomainfrom
feat/dynamic-connection-form
Mar 13, 2026
Merged

feat: make ConnectionFormView fully dynamic via plugin metadata#309
datlechin merged 4 commits intomainfrom
feat/dynamic-connection-form

Conversation

@datlechin
Copy link
Copy Markdown
Collaborator

@datlechin datlechin commented Mar 13, 2026

Summary

  • Add FieldSection, hidesPassword to ConnectionField and supportsSSH/supportsSSL to DriverPlugin protocol so plugins can control connection form behavior
  • Remove ~12 hardcoded pluginTypeId checks from ConnectionFormView (pgpass toggle, password visibility, SSH/SSL tab visibility, Redis database persistence)
  • Simplify DatabaseDriverFactory.resolvePassword() by removing PostgreSQL/Redshift type guard — usePgpass can only be true if a plugin declares it
  • PostgreSQL plugin now declares pgpass as an authentication-section field with hidesPassword: true
  • SQLite plugin now declares supportsSSH = false, supportsSSL = false
  • Only 1 hardcoded check remains: Redis URL parsing (db index in path segment)

Test plan

  • Build succeeds (xcodebuild -scheme TablePro build -skipPackagePluginValidation)
  • Open connection form, switch to PostgreSQL — pgpass toggle appears in Authentication section
  • Enable pgpass toggle — password field hides, pgpass status view shows
  • Switch to MySQL — pgpass toggle disappears, password field returns
  • Switch to SQLite — SSH and SSL tabs are hidden, only General and Advanced shown
  • Switch to PostgreSQL/MySQL — SSH and SSL tabs reappear
  • Create/edit Redis connection — database index persists correctly via additionalFields
  • grep -rn 'pluginTypeId == "PostgreSQL"' TablePro/Views/Connection/ returns no results

Summary by CodeRabbit

  • New Features

    • Added a "Use ~/.pgpass" toggle for PostgreSQL authentication.
    • Authentication fields are now driven by plugin-provided metadata; SSH/SSL tabs shown per driver capability.
    • Connection fields are dynamically defined for consistent cross-database behavior.
  • Bug Fixes

    • Password visibility now respects auth field settings (including pgpass) across types.
    • Redis database value storage and migration unified.
    • Form state, save, and test flows made more consistent and reliable.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 13, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 555a5938-0c9c-4630-a7d1-6f68ad56f422

📥 Commits

Reviewing files that changed from the base of the PR and between fe262a8 and b5fe3ad.

📒 Files selected for processing (1)
  • TablePro/Views/Connection/ConnectionFormView.swift

📝 Walkthrough

Walkthrough

Connection UI and plugin metadata were made field-driven: ConnectionField gains section and hidesPassword; DriverPlugin gains supportsSSH/supportsSSL; PluginManager, ConnectionFormView, DatabaseDriver, and two plugins were updated to use these new fields/capabilities (including a pgpass toggle and unified redisDatabase storage).

Changes

Cohort / File(s) Summary
Plugin Kit Infrastructure
Plugins/TableProPluginKit/ConnectionField.swift, Plugins/TableProPluginKit/DriverPlugin.swift
Adds FieldSection enum and section/hidesPassword to ConnectionField (initializer + decoder); adds supportsSSH/supportsSSL requirements with default implementations on DriverPlugin.
Plugin Implementations
Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift, Plugins/SQLiteDriverPlugin/SQLitePlugin.swift
Postgres plugin adds an authentication-section toggle usePgpass (hides password); SQLite plugin declares supportsSSH and supportsSSL as false.
Plugin Management
TablePro/Core/Plugins/PluginManager.swift
Adds supportsSSH(for:) and supportsSSL(for:) helpers that resolve plugin types and return their static capability (defaults to true when plugin missing).
Connection Logic
TablePro/Core/Database/DatabaseDriver.swift
Password resolution now treats usePgpass from additional fields as a global suppressor of returned password (not limited to PostgreSQL/Redshift).
Connection Form UI
TablePro/Views/Connection/ConnectionFormView.swift
Refactors form to derive auth fields from plugin metadata, adds authSectionFields, hidePasswordField, computed usePgpass from additional fields, capability-driven SSH/SSL tabs, unified redisDatabase storage as string, and field-driven bindings/rendering.
Changelog
CHANGELOG.md
Documents the ConnectionFormView dynamic behavior driven by FieldSection, hidesPassword, supportsSSH, and supportsSSL.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant Form as ConnectionFormView
    participant PM as PluginManager
    participant Plugin as DriverPlugin
    participant DB as DatabaseDriver

    User->>Form: Open/select database type
    Form->>PM: supportsSSH(for: type)
    PM->>Plugin: resolve plugin & query supportsSSH
    Plugin-->>PM: Bool
    PM-->>Form: capability

    Form->>PM: supportsSSL(for: type)
    PM->>Plugin: resolve plugin & query supportsSSL
    Plugin-->>PM: Bool
    PM-->>Form: capability

    Form->>PM: additionalConnectionFields(for: type)
    PM->>Plugin: fetch field definitions (section, hidesPassword)
    Plugin-->>PM: fields
    PM-->>Form: fields

    User->>Form: toggle auth fields (e.g., usePgpass)
    Form->>Form: update additionalFieldValues
    Form->>DB: request resolved password (reads additionalFieldValues["usePgpass"])
    DB->>DB: if usePgpass == "true" then return empty password
    DB-->>Form: resolved password
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through fields both new and old,

Toggles tucked secrets, quiet and bold.
Sections stacked neat like carrots in line,
Metadata hummed, the form worked fine.
A cheerful nibble, then off I stroll. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 'feat: make ConnectionFormView fully dynamic via plugin metadata' directly and clearly summarizes the main objective of the changeset: refactoring ConnectionFormView to be driven by plugin metadata instead of hardcoded logic.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/dynamic-connection-form
📝 Coding Plan
  • Generate coding plan for human review comments

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

Copy link
Copy Markdown

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
TablePro/Views/Connection/ConnectionFormView.swift (3)

661-675: ⚠️ Potential issue | 🟠 Major

Filter the Advanced tab to .advanced fields.

additionalConnectionFields now includes authentication-section entries too, so the PostgreSQL usePgpass toggle renders twice: once under Authentication and again here. Split the list by section before building the Advanced tab.

🧭 Suggested change
-            if !additionalConnectionFields.isEmpty {
+            let advancedFields = additionalConnectionFields.filter { $0.section == .advanced }
+            if !advancedFields.isEmpty {
                 Section(type.displayName) {
-                    ForEach(additionalConnectionFields, id: \.id) { field in
+                    ForEach(advancedFields, id: \.id) { field in
                         ConnectionFieldRow(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Views/Connection/ConnectionFormView.swift` around lines 661 - 675,
The Advanced section is using additionalConnectionFields which currently
contains authentication entries too, causing duplicates; update the
Section(type.displayName) block to only iterate over additionalConnectionFields
filtered by their section (e.g., field.section == .advanced) before creating
ConnectionFieldRow, so only fields belonging to the .advanced section are shown;
locate the Section/ForEach that references additionalConnectionFields and apply
the filter there (using the field.section enum value) so the PostgreSQL
usePgpass toggle is not rendered twice.

172-175: ⚠️ Potential issue | 🟡 Minor

Capability-based tabs need a capability-based fallback.

This still only falls back for file-based drivers. If selectedTab is .ssh or .ssl and the user switches to a driver that disables just that capability, selectedTab still points at the hidden tab and tabForm will keep rendering unsupported content.

🔧 Minimal fix
-            let isFileBased = PluginManager.shared.connectionMode(for: newType) == .fileBased
-            if isFileBased && (selectedTab == .ssh || selectedTab == .ssl) {
+            if !visibleTabs.contains(selectedTab) {
                 selectedTab = .general
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Views/Connection/ConnectionFormView.swift` around lines 172 - 175,
The selectedTab fallback only checks for file-based drivers; change it to
validate whether the newly selected driver actually supports the currently
selected tab's capability (e.g., if selectedTab is .ssh verify SSH is supported,
if .ssl verify TLS/SSL is supported) by querying
PluginManager.shared.connectionMode(for: newType) or the driver's capability
flags, and if the capability is not supported set selectedTab = .general; update
the logic around selectedTab (the block that currently only checks isFileBased)
so any unsupported-capability tab is redirected to .general before tabForm
renders.

168-182: ⚠️ Potential issue | 🟠 Major

This onChange(of: type) is clobbering restored/imported state.

loadConnectionData() and parseConnectionURL() both assign type programmatically. On edit load, this handler rebuilds additionalFieldValues and can immediately drop restored plugin fields like usePgpass, redisDatabase, or mongoAuthSource; once the form is already loaded, URL import can also reset a parsed custom port back to the driver default. These resets need to stay on the picker-driven path only, or be suppressed while loading/importing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Views/Connection/ConnectionFormView.swift` around lines 168 - 182,
The onChange(of: type) handler is running for programmatic assignments (from
loadConnectionData()/parseConnectionURL()) and clobbering restored fields;
introduce a boolean flag (e.g., isApplyingConnectionData) that
loadConnectionData() and parseConnectionURL() set true before they assign type
and set false after, then in the onChange(of: type) early-return (or skip the
port/defaults/selectedTab/additionalFieldValues mutations) when
isApplyingConnectionData is true so only picker-driven changes perform those
resets; keep existing hasLoadedData logic for initial-load behavior but suppress
the default-port and additionalFieldValues rebuild while
isApplyingConnectionData is set.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Plugins/TableProPluginKit/DriverPlugin.swift`:
- Around line 49-50: The DriverPlugin protocol gained new requirements (static
var supportsSSH and static var supportsSSL) which breaks binary compatibility
for runtime-loaded plugins, so bump the plugin-kit compatibility version used by
the runtime check: locate the currentPluginKitVersion constant referenced by
PluginManager’s plugin validation and increment it from 1 (or 0) to 2; keep the
new default implementations on DriverPlugin but ensure PluginManager’s version
check rejects older/unversioned bundles by using the new value so plugins
compiled against the prior protocol shape are not loaded.

---

Outside diff comments:
In `@TablePro/Views/Connection/ConnectionFormView.swift`:
- Around line 661-675: The Advanced section is using additionalConnectionFields
which currently contains authentication entries too, causing duplicates; update
the Section(type.displayName) block to only iterate over
additionalConnectionFields filtered by their section (e.g., field.section ==
.advanced) before creating ConnectionFieldRow, so only fields belonging to the
.advanced section are shown; locate the Section/ForEach that references
additionalConnectionFields and apply the filter there (using the field.section
enum value) so the PostgreSQL usePgpass toggle is not rendered twice.
- Around line 172-175: The selectedTab fallback only checks for file-based
drivers; change it to validate whether the newly selected driver actually
supports the currently selected tab's capability (e.g., if selectedTab is .ssh
verify SSH is supported, if .ssl verify TLS/SSL is supported) by querying
PluginManager.shared.connectionMode(for: newType) or the driver's capability
flags, and if the capability is not supported set selectedTab = .general; update
the logic around selectedTab (the block that currently only checks isFileBased)
so any unsupported-capability tab is redirected to .general before tabForm
renders.
- Around line 168-182: The onChange(of: type) handler is running for
programmatic assignments (from loadConnectionData()/parseConnectionURL()) and
clobbering restored fields; introduce a boolean flag (e.g.,
isApplyingConnectionData) that loadConnectionData() and parseConnectionURL() set
true before they assign type and set false after, then in the onChange(of: type)
early-return (or skip the port/defaults/selectedTab/additionalFieldValues
mutations) when isApplyingConnectionData is true so only picker-driven changes
perform those resets; keep existing hasLoadedData logic for initial-load
behavior but suppress the default-port and additionalFieldValues rebuild while
isApplyingConnectionData is set.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69ce6856-4721-4ff1-94a9-5983122ed5fd

📥 Commits

Reviewing files that changed from the base of the PR and between 9cb99a9 and 7a47c52.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift
  • Plugins/SQLiteDriverPlugin/SQLitePlugin.swift
  • Plugins/TableProPluginKit/ConnectionField.swift
  • Plugins/TableProPluginKit/DriverPlugin.swift
  • TablePro/Core/Database/DatabaseDriver.swift
  • TablePro/Core/Plugins/PluginManager.swift
  • TablePro/Views/Connection/ConnectionFormView.swift

Copy link
Copy Markdown

@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: 3

🧹 Nitpick comments (1)
TablePro/Views/Connection/ConnectionFormView.swift (1)

853-854: Remove duplicated/restating migration comment.

Lines 853-854 repeat the same intent and restate obvious code behavior. Keep one concise rationale-only comment.

As per coding guidelines: "Do not add comments that restate what the code says; only comment to explain non-obvious 'why' reasoning or clarify genuinely complex logic".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Views/Connection/ConnectionFormView.swift` around lines 853 - 854,
Remove the duplicated migration comment around the conditional that checks
additionalFieldValues["redisDatabase"] (the block that migrates legacy
redisDatabase to additionalFields); keep a single concise rationale-only comment
explaining why the migration is needed and delete the repeated/restating comment
so comments do not restate the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@TablePro/Views/Connection/ConnectionFormView.swift`:
- Line 935: The redisDatabase parsing can yield nil for non-numeric strings;
change occurrences where you set redisDatabase (in the ConnectionFormView
initializer/mapping and in testConnection()) from redisDatabase:
Int(additionalFieldValues["redisDatabase"] ?? "0") to redisDatabase:
(Int(additionalFieldValues["redisDatabase"] ?? "0") ?? 0) so invalid values fall
back to 0 instead of nil; update both the mapping at the shown diff and the
testConnection() usage.
- Around line 297-301: authSectionFields are being rendered in the
Authentication section and then again because additionalConnectionFields is
rendered unchanged in the Advanced section; update the Advanced rendering to
exclude fields already shown in Authentication by filtering
additionalConnectionFields before rendering (e.g.,
additionalConnectionFields.filter { field in !authSectionFields.contains(where:
{ $0.id == field.id }) } ), so fields like usePgpass are not duplicated; ensure
bindings (toggleBinding(for:)) still resolve for the remaining fields.
- Around line 189-197: After filtering tabs by
PluginManager.shared.supportsSSH(for:) and supportsSSL(for:) you must normalize
the view's selectedTab so it can't point to a now-hidden tab; after building the
tabs array check whether the current selectedTab (property selectedTab in
ConnectionFormView) exists in the resulting tabs (FormTab values like .ssh,
.ssl, .general, .advanced) and if not set selectedTab to a safe default (e.g.
.general or tabs.first!). Implement this normalization immediately before
returning tabs in the same getter/function that constructs the tabs array.

---

Nitpick comments:
In `@TablePro/Views/Connection/ConnectionFormView.swift`:
- Around line 853-854: Remove the duplicated migration comment around the
conditional that checks additionalFieldValues["redisDatabase"] (the block that
migrates legacy redisDatabase to additionalFields); keep a single concise
rationale-only comment explaining why the migration is needed and delete the
repeated/restating comment so comments do not restate the code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f91e681-24a9-4b4e-9796-8a182dacb5d5

📥 Commits

Reviewing files that changed from the base of the PR and between 7a47c52 and 3179167.

📒 Files selected for processing (2)
  • Plugins/TableProPluginKit/ConnectionField.swift
  • TablePro/Views/Connection/ConnectionFormView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Plugins/TableProPluginKit/ConnectionField.swift

Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@TablePro/Views/Connection/ConnectionFormView.swift`:
- Around line 934-935: The code currently coerces a missing
additionalFieldValues["redisDatabase"] into 0; change both the save and test
builder code paths in ConnectionFormView (where additionalFieldValues is read to
produce redisDatabase and where startupCommands is trimmed) to treat an absent
key as nil and only fall back to 0 when the key exists but cannot be parsed as
an Int; specifically, when constructing the redisDatabase value from
additionalFieldValues["redisDatabase"] return nil if the dictionary lookup
yields nil, otherwise attempt Int(...) and use 0 only on parse failure so
non-Redis connections do not get backfilled with "0" during the migration.
- Around line 296-300: The Authentication fields loop currently only renders
.toggle types (ForEach over authSectionFields with if field.fieldType ==
.toggle) so other plugin-provided field types are dropped; update the rendering
to handle all field.fieldType cases by switching on field.fieldType (or
delegating to a helper like viewForField(_:) that returns the appropriate
SwiftUI view) and keep using toggleBinding(for:) only for .toggle while wiring
textBinding/secureBinding/etc. for other types; also ensure the
authSectionFields/advanced filtering logic that produces authSectionFields
doesn’t mistakenly filter out non-.toggle non-.advanced fields so plugin fields
appear in the correct Authentication or Advanced section.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ae0685a3-812d-489b-a9fa-1852d653248f

📥 Commits

Reviewing files that changed from the base of the PR and between 3179167 and fe262a8.

📒 Files selected for processing (1)
  • TablePro/Views/Connection/ConnectionFormView.swift

@datlechin datlechin merged commit aeb3aea into main Mar 13, 2026
2 of 3 checks passed
@datlechin datlechin deleted the feat/dynamic-connection-form branch March 13, 2026 08:55
datlechin added a commit that referenced this pull request Mar 13, 2026
* feat: make ConnectionFormView fully dynamic via plugin metadata (#309)

* fix: remove dead code, restore Redis default, add Codable backward compat

* fix: address PR review — filter Advanced tab, normalize selected tab, harden redis parsing

* fix: use ConnectionFieldRow for auth fields, preserve nil redisDatabase for non-Redis

* feat: add runtime validation of plugin driver descriptors

* fix: address PR review — fix partial registration, wire up dead code, rewrite tests

* fix: wire up validateDialectDescriptor, serialize mock-based test suite
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