Skip to content

Commit aeb3aea

Browse files
authored
feat: make ConnectionFormView fully dynamic via plugin metadata (#309)
* 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
1 parent 9cb99a9 commit aeb3aea

File tree

8 files changed

+100
-42
lines changed

8 files changed

+100
-42
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Changed
1111

1212
- Replaced ~40 hardcoded `DatabaseType` switches across ~20 UI files with dynamic plugin property lookups via `PluginManager`, so third-party plugins get correct UI behavior (colors, labels, editor language, feature toggles) automatically
13+
- ConnectionFormView now fully dynamic: pgpass toggle, password visibility, and SSH/SSL tab visibility all driven by plugin metadata (`FieldSection`, `hidesPassword`, `supportsSSH`/`supportsSSL`) instead of hardcoded type checks
1314
- Replaced `AppState.isMongoDB`/`isRedis` booleans with `AppState.editorLanguage: EditorLanguage` for extensible editor language detection
1415
- Theme colors now derived from plugin `brandColorHex` instead of hardcoded `Theme.mysqlColor` etc.
1516
- Sidebar labels ("Tables"/"Collections"/"Keys"), toolbar preview labels, and AI prompt language detection now use plugin metadata

Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,16 @@ final class PostgreSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
2121
static let databaseDisplayName = "PostgreSQL"
2222
static let iconName = "cylinder.fill"
2323
static let defaultPort = 5432
24-
static let additionalConnectionFields: [ConnectionField] = []
24+
static let additionalConnectionFields: [ConnectionField] = [
25+
ConnectionField(
26+
id: "usePgpass",
27+
label: String(localized: "Use ~/.pgpass"),
28+
defaultValue: "false",
29+
fieldType: .toggle,
30+
section: .authentication,
31+
hidesPassword: true
32+
)
33+
]
2534
static let additionalDatabaseTypeIds: [String] = ["Redshift"]
2635

2736
// MARK: - UI/Capability Metadata

Plugins/SQLiteDriverPlugin/SQLitePlugin.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ final class SQLitePlugin: NSObject, TableProPlugin, DriverPlugin {
2222
// MARK: - UI/Capability Metadata
2323

2424
static let requiresAuthentication = false
25+
static let supportsSSH = false
26+
static let supportsSSL = false
2527
static let connectionMode: ConnectionMode = .fileBased
2628
static let urlSchemes: [String] = ["sqlite"]
2729
static let fileExtensions: [String] = ["db", "sqlite", "sqlite3"]

Plugins/TableProPluginKit/ConnectionField.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import Foundation
22

3+
public enum FieldSection: String, Codable, Sendable {
4+
case authentication
5+
case advanced
6+
}
7+
38
public struct ConnectionField: Codable, Sendable {
49
public struct IntRange: Codable, Sendable, Equatable {
510
public let lowerBound: Int
@@ -70,6 +75,8 @@ public struct ConnectionField: Codable, Sendable {
7075
public let isRequired: Bool
7176
public let defaultValue: String?
7277
public let fieldType: FieldType
78+
public let section: FieldSection
79+
public let hidesPassword: Bool
7380

7481
/// Backward-compatible convenience: true when fieldType is .secure
7582
public var isSecure: Bool {
@@ -84,13 +91,33 @@ public struct ConnectionField: Codable, Sendable {
8491
required: Bool = false,
8592
secure: Bool = false,
8693
defaultValue: String? = nil,
87-
fieldType: FieldType? = nil
94+
fieldType: FieldType? = nil,
95+
section: FieldSection = .advanced,
96+
hidesPassword: Bool = false
8897
) {
8998
self.id = id
9099
self.label = label
91100
self.placeholder = placeholder
92101
self.isRequired = required
93102
self.defaultValue = defaultValue
94103
self.fieldType = fieldType ?? (secure ? .secure : .text)
104+
self.section = section
105+
self.hidesPassword = hidesPassword
106+
}
107+
108+
public init(from decoder: Decoder) throws {
109+
let container = try decoder.container(keyedBy: CodingKeys.self)
110+
id = try container.decode(String.self, forKey: .id)
111+
label = try container.decode(String.self, forKey: .label)
112+
placeholder = try container.decodeIfPresent(String.self, forKey: .placeholder) ?? ""
113+
isRequired = try container.decodeIfPresent(Bool.self, forKey: .isRequired) ?? false
114+
defaultValue = try container.decodeIfPresent(String.self, forKey: .defaultValue)
115+
fieldType = try container.decode(FieldType.self, forKey: .fieldType)
116+
section = try container.decodeIfPresent(FieldSection.self, forKey: .section) ?? .advanced
117+
hidesPassword = try container.decodeIfPresent(Bool.self, forKey: .hidesPassword) ?? false
118+
}
119+
120+
private enum CodingKeys: String, CodingKey {
121+
case id, label, placeholder, isRequired, defaultValue, fieldType, section, hidesPassword
95122
}
96123
}

Plugins/TableProPluginKit/DriverPlugin.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public protocol DriverPlugin: TableProPlugin {
4646
static var structureColumnFields: [StructureColumnField] { get }
4747
static var defaultPrimaryKeyColumn: String? { get }
4848
static var supportsQueryProgress: Bool { get }
49+
static var supportsSSH: Bool { get }
50+
static var supportsSSL: Bool { get }
4951
}
5052

5153
public extension DriverPlugin {
@@ -98,4 +100,6 @@ public extension DriverPlugin {
98100
}
99101
static var defaultPrimaryKeyColumn: String? { nil }
100102
static var supportsQueryProgress: Bool { false }
103+
static var supportsSSH: Bool { true }
104+
static var supportsSSL: Bool { true }
101105
}

TablePro/Core/Database/DatabaseDriver.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -352,9 +352,7 @@ enum DatabaseDriverFactory {
352352
}
353353

354354
private static func resolvePassword(for connection: DatabaseConnection) -> String {
355-
if connection.usePgpass
356-
&& (connection.type == .postgresql || connection.type == .redshift)
357-
{
355+
if connection.usePgpass {
358356
return ""
359357
}
360358
return ConnectionStorage.shared.loadPassword(for: connection.id) ?? ""

TablePro/Core/Plugins/PluginManager.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,16 @@ final class PluginManager {
494494
return Swift.type(of: plugin).supportsQueryProgress
495495
}
496496

497+
func supportsSSH(for databaseType: DatabaseType) -> Bool {
498+
guard let plugin = driverPlugin(for: databaseType) else { return true }
499+
return Swift.type(of: plugin).supportsSSH
500+
}
501+
502+
func supportsSSL(for databaseType: DatabaseType) -> Bool {
503+
guard let plugin = driverPlugin(for: databaseType) else { return true }
504+
return Swift.type(of: plugin).supportsSSL
505+
}
506+
497507
func autoLimitStyle(for databaseType: DatabaseType) -> AutoLimitStyle {
498508
guard let plugin = driverPlugin(for: databaseType) else { return .limit }
499509
guard let dialect = Swift.type(of: plugin).sqlDialect else { return .none }

TablePro/Views/Connection/ConnectionFormView.swift

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ struct ConnectionFormView: View {
3232
PluginManager.shared.additionalConnectionFields(for: type)
3333
}
3434

35+
private var authSectionFields: [ConnectionField] {
36+
PluginManager.shared.additionalConnectionFields(for: type)
37+
.filter { $0.section == .authentication }
38+
}
39+
40+
private var hidePasswordField: Bool {
41+
authSectionFields.contains { $0.hidesPassword && additionalFieldValues[$0.id] == "true" }
42+
}
43+
3544
@State private var name: String = ""
3645
@State private var host: String = ""
3746
@State private var port: String = ""
@@ -83,9 +92,12 @@ struct ConnectionFormView: View {
8392
@State private var startupCommands: String = ""
8493

8594
// Pgpass
86-
@State private var usePgpass: Bool = false
8795
@State private var pgpassStatus: PgpassStatus = .notChecked
8896

97+
private var usePgpass: Bool {
98+
additionalFieldValues["usePgpass"] == "true"
99+
}
100+
89101
// Pre-connect script
90102
@State private var preConnectScript: String = ""
91103

@@ -143,14 +155,10 @@ struct ConnectionFormView: View {
143155
if hasLoadedData {
144156
port = String(newType.defaultPort)
145157
}
146-
let isFileBased = PluginManager.shared.connectionMode(for: newType) == .fileBased
147-
if isFileBased && (selectedTab == .ssh || selectedTab == .ssl) {
158+
if !visibleTabs.contains(selectedTab) {
148159
selectedTab = .general
149160
}
150161
additionalFieldValues = [:]
151-
if newType.pluginTypeId != "PostgreSQL" {
152-
usePgpass = false
153-
}
154162
for field in PluginManager.shared.additionalConnectionFields(for: newType) {
155163
if let defaultValue = field.defaultValue {
156164
additionalFieldValues[field.id] = defaultValue
@@ -160,7 +168,7 @@ struct ConnectionFormView: View {
160168
.pluginInstallPrompt(connection: $pluginInstallConnection) { connection in
161169
connectAfterInstall(connection)
162170
}
163-
.onChange(of: usePgpass) { _, _ in updatePgpassStatus() }
171+
.onChange(of: additionalFieldValues) { _, _ in updatePgpassStatus() }
164172
.onChange(of: host) { _, _ in updatePgpassStatus() }
165173
.onChange(of: port) { _, _ in updatePgpassStatus() }
166174
.onChange(of: database) { _, _ in updatePgpassStatus() }
@@ -170,10 +178,15 @@ struct ConnectionFormView: View {
170178
// MARK: - Tab Picker Helpers
171179

172180
private var visibleTabs: [FormTab] {
173-
if PluginManager.shared.connectionMode(for: type) == .fileBased {
174-
return [.general, .advanced]
181+
var tabs: [FormTab] = [.general]
182+
if PluginManager.shared.supportsSSH(for: type) {
183+
tabs.append(.ssh)
184+
}
185+
if PluginManager.shared.supportsSSL(for: type) {
186+
tabs.append(.ssl)
175187
}
176-
return FormTab.allCases
188+
tabs.append(.advanced)
189+
return tabs
177190
}
178191

179192
private var resolvedSSHAgentSocketPath: String {
@@ -273,16 +286,25 @@ struct ConnectionFormView: View {
273286
prompt: Text("root")
274287
)
275288
}
276-
if type.pluginTypeId == "PostgreSQL" {
277-
Toggle(String(localized: "Use ~/.pgpass"), isOn: $usePgpass)
289+
ForEach(authSectionFields, id: \.id) { field in
290+
ConnectionFieldRow(
291+
field: field,
292+
value: Binding(
293+
get: {
294+
additionalFieldValues[field.id]
295+
?? field.defaultValue ?? ""
296+
},
297+
set: { additionalFieldValues[field.id] = $0 }
298+
)
299+
)
278300
}
279-
if !usePgpass || type.pluginTypeId != "PostgreSQL" {
301+
if !hidePasswordField {
280302
SecureField(
281303
String(localized: "Password"),
282304
text: $password
283305
)
284306
}
285-
if usePgpass && type.pluginTypeId == "PostgreSQL" {
307+
if additionalFieldValues["usePgpass"] == "true" {
286308
pgpassStatusView
287309
}
288310
}
@@ -628,9 +650,10 @@ struct ConnectionFormView: View {
628650

629651
private var advancedForm: some View {
630652
Form {
631-
if !additionalConnectionFields.isEmpty {
653+
let advancedFields = additionalConnectionFields.filter { $0.section == .advanced }
654+
if !advancedFields.isEmpty {
632655
Section(type.displayName) {
633-
ForEach(additionalConnectionFields, id: \.id) { field in
656+
ForEach(advancedFields, id: \.id) { field in
634657
ConnectionFieldRow(
635658
field: field,
636659
value: Binding(
@@ -775,7 +798,7 @@ struct ConnectionFormView: View {
775798
}
776799

777800
private func updatePgpassStatus() {
778-
guard usePgpass, type.pluginTypeId == "PostgreSQL" else {
801+
guard additionalFieldValues["usePgpass"] == "true" else {
779802
pgpassStatus = .notChecked
780803
return
781804
}
@@ -826,9 +849,8 @@ struct ConnectionFormView: View {
826849
// Load additional fields from connection
827850
additionalFieldValues = existing.additionalFields
828851

829-
// Migrate legacy Redis database index before default seeding
830-
if existing.type.pluginTypeId == "Redis",
831-
additionalFieldValues["redisDatabase"] == nil,
852+
// Migrate legacy redisDatabase to additionalFields
853+
if additionalFieldValues["redisDatabase"] == nil,
832854
let rdb = existing.redisDatabase {
833855
additionalFieldValues["redisDatabase"] = String(rdb)
834856
}
@@ -841,7 +863,6 @@ struct ConnectionFormView: View {
841863

842864
// Load startup commands
843865
startupCommands = existing.startupCommands ?? ""
844-
usePgpass = existing.usePgpass
845866
preConnectScript = existing.preConnectScript ?? ""
846867

847868
// Load passwords from Keychain
@@ -888,11 +909,6 @@ struct ConnectionFormView: View {
888909
? "root" : trimmedUsername
889910

890911
var finalAdditionalFields = additionalFieldValues
891-
if usePgpass && type.pluginTypeId == "PostgreSQL" {
892-
finalAdditionalFields["usePgpass"] = "true"
893-
} else {
894-
finalAdditionalFields.removeValue(forKey: "usePgpass")
895-
}
896912
let trimmedScript = preConnectScript.trimmingCharacters(in: .whitespacesAndNewlines)
897913
if !trimmedScript.isEmpty {
898914
finalAdditionalFields["preConnectScript"] = preConnectScript
@@ -915,9 +931,7 @@ struct ConnectionFormView: View {
915931
groupId: selectedGroupId,
916932
safeModeLevel: safeModeLevel,
917933
aiPolicy: aiPolicy,
918-
redisDatabase: type.pluginTypeId == "Redis"
919-
? Int(additionalFieldValues["redisDatabase"] ?? "0")
920-
: nil,
934+
redisDatabase: additionalFieldValues["redisDatabase"].map { Int($0) ?? 0 },
921935
startupCommands: startupCommands.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
922936
? nil : startupCommands,
923937
additionalFields: finalAdditionalFields.isEmpty ? nil : finalAdditionalFields
@@ -1047,11 +1061,6 @@ struct ConnectionFormView: View {
10471061
? "root" : trimmedUsername
10481062

10491063
var finalAdditionalFields = additionalFieldValues
1050-
if usePgpass && type.pluginTypeId == "PostgreSQL" {
1051-
finalAdditionalFields["usePgpass"] = "true"
1052-
} else {
1053-
finalAdditionalFields.removeValue(forKey: "usePgpass")
1054-
}
10551064
let trimmedScript = preConnectScript.trimmingCharacters(in: .whitespacesAndNewlines)
10561065
if !trimmedScript.isEmpty {
10571066
finalAdditionalFields["preConnectScript"] = preConnectScript
@@ -1071,9 +1080,7 @@ struct ConnectionFormView: View {
10711080
color: connectionColor,
10721081
tagId: selectedTagId,
10731082
groupId: selectedGroupId,
1074-
redisDatabase: type.pluginTypeId == "Redis"
1075-
? Int(additionalFieldValues["redisDatabase"] ?? "0")
1076-
: nil,
1083+
redisDatabase: additionalFieldValues["redisDatabase"].map { Int($0) ?? 0 },
10771084
startupCommands: startupCommands.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
10781085
? nil : startupCommands,
10791086
additionalFields: finalAdditionalFields.isEmpty ? nil : finalAdditionalFields

0 commit comments

Comments
 (0)