Skip to content

Commit 7a47c52

Browse files
committed
feat: make ConnectionFormView fully dynamic via plugin metadata (#309)
1 parent 9cb99a9 commit 7a47c52

8 files changed

Lines changed: 87 additions & 37 deletions

File tree

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: 12 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,17 @@ 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
95106
}
96107
}

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: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ 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+
44+
private func toggleBinding(for fieldId: String) -> Binding<Bool> {
45+
Binding(
46+
get: { additionalFieldValues[fieldId] == "true" },
47+
set: { additionalFieldValues[fieldId] = $0 ? "true" : "false" }
48+
)
49+
}
50+
3551
@State private var name: String = ""
3652
@State private var host: String = ""
3753
@State private var port: String = ""
@@ -83,9 +99,19 @@ struct ConnectionFormView: View {
8399
@State private var startupCommands: String = ""
84100

85101
// Pgpass
86-
@State private var usePgpass: Bool = false
87102
@State private var pgpassStatus: PgpassStatus = .notChecked
88103

104+
private var usePgpass: Bool {
105+
additionalFieldValues["usePgpass"] == "true"
106+
}
107+
108+
private var usePgpassBinding: Binding<Bool> {
109+
Binding(
110+
get: { additionalFieldValues["usePgpass"] == "true" },
111+
set: { additionalFieldValues["usePgpass"] = $0 ? "true" : "false" }
112+
)
113+
}
114+
89115
// Pre-connect script
90116
@State private var preConnectScript: String = ""
91117

@@ -148,9 +174,6 @@ struct ConnectionFormView: View {
148174
selectedTab = .general
149175
}
150176
additionalFieldValues = [:]
151-
if newType.pluginTypeId != "PostgreSQL" {
152-
usePgpass = false
153-
}
154177
for field in PluginManager.shared.additionalConnectionFields(for: newType) {
155178
if let defaultValue = field.defaultValue {
156179
additionalFieldValues[field.id] = defaultValue
@@ -160,7 +183,7 @@ struct ConnectionFormView: View {
160183
.pluginInstallPrompt(connection: $pluginInstallConnection) { connection in
161184
connectAfterInstall(connection)
162185
}
163-
.onChange(of: usePgpass) { _, _ in updatePgpassStatus() }
186+
.onChange(of: additionalFieldValues) { _, _ in updatePgpassStatus() }
164187
.onChange(of: host) { _, _ in updatePgpassStatus() }
165188
.onChange(of: port) { _, _ in updatePgpassStatus() }
166189
.onChange(of: database) { _, _ in updatePgpassStatus() }
@@ -170,10 +193,15 @@ struct ConnectionFormView: View {
170193
// MARK: - Tab Picker Helpers
171194

172195
private var visibleTabs: [FormTab] {
173-
if PluginManager.shared.connectionMode(for: type) == .fileBased {
174-
return [.general, .advanced]
196+
var tabs: [FormTab] = [.general]
197+
if PluginManager.shared.supportsSSH(for: type) {
198+
tabs.append(.ssh)
199+
}
200+
if PluginManager.shared.supportsSSL(for: type) {
201+
tabs.append(.ssl)
175202
}
176-
return FormTab.allCases
203+
tabs.append(.advanced)
204+
return tabs
177205
}
178206

179207
private var resolvedSSHAgentSocketPath: String {
@@ -273,16 +301,18 @@ struct ConnectionFormView: View {
273301
prompt: Text("root")
274302
)
275303
}
276-
if type.pluginTypeId == "PostgreSQL" {
277-
Toggle(String(localized: "Use ~/.pgpass"), isOn: $usePgpass)
304+
ForEach(authSectionFields, id: \.id) { field in
305+
if field.fieldType == .toggle {
306+
Toggle(field.label, isOn: toggleBinding(for: field.id))
307+
}
278308
}
279-
if !usePgpass || type.pluginTypeId != "PostgreSQL" {
309+
if !hidePasswordField {
280310
SecureField(
281311
String(localized: "Password"),
282312
text: $password
283313
)
284314
}
285-
if usePgpass && type.pluginTypeId == "PostgreSQL" {
315+
if additionalFieldValues["usePgpass"] == "true" {
286316
pgpassStatusView
287317
}
288318
}
@@ -775,7 +805,7 @@ struct ConnectionFormView: View {
775805
}
776806

777807
private func updatePgpassStatus() {
778-
guard usePgpass, type.pluginTypeId == "PostgreSQL" else {
808+
guard additionalFieldValues["usePgpass"] == "true" else {
779809
pgpassStatus = .notChecked
780810
return
781811
}
@@ -827,8 +857,8 @@ struct ConnectionFormView: View {
827857
additionalFieldValues = existing.additionalFields
828858

829859
// Migrate legacy Redis database index before default seeding
830-
if existing.type.pluginTypeId == "Redis",
831-
additionalFieldValues["redisDatabase"] == nil,
860+
// Migrate legacy redisDatabase to additionalFields
861+
if additionalFieldValues["redisDatabase"] == nil,
832862
let rdb = existing.redisDatabase {
833863
additionalFieldValues["redisDatabase"] = String(rdb)
834864
}
@@ -841,7 +871,6 @@ struct ConnectionFormView: View {
841871

842872
// Load startup commands
843873
startupCommands = existing.startupCommands ?? ""
844-
usePgpass = existing.usePgpass
845874
preConnectScript = existing.preConnectScript ?? ""
846875

847876
// Load passwords from Keychain
@@ -888,11 +917,6 @@ struct ConnectionFormView: View {
888917
? "root" : trimmedUsername
889918

890919
var finalAdditionalFields = additionalFieldValues
891-
if usePgpass && type.pluginTypeId == "PostgreSQL" {
892-
finalAdditionalFields["usePgpass"] = "true"
893-
} else {
894-
finalAdditionalFields.removeValue(forKey: "usePgpass")
895-
}
896920
let trimmedScript = preConnectScript.trimmingCharacters(in: .whitespacesAndNewlines)
897921
if !trimmedScript.isEmpty {
898922
finalAdditionalFields["preConnectScript"] = preConnectScript
@@ -915,9 +939,7 @@ struct ConnectionFormView: View {
915939
groupId: selectedGroupId,
916940
safeModeLevel: safeModeLevel,
917941
aiPolicy: aiPolicy,
918-
redisDatabase: type.pluginTypeId == "Redis"
919-
? Int(additionalFieldValues["redisDatabase"] ?? "0")
920-
: nil,
942+
redisDatabase: Int(additionalFieldValues["redisDatabase"] ?? ""),
921943
startupCommands: startupCommands.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
922944
? nil : startupCommands,
923945
additionalFields: finalAdditionalFields.isEmpty ? nil : finalAdditionalFields
@@ -1047,11 +1069,6 @@ struct ConnectionFormView: View {
10471069
? "root" : trimmedUsername
10481070

10491071
var finalAdditionalFields = additionalFieldValues
1050-
if usePgpass && type.pluginTypeId == "PostgreSQL" {
1051-
finalAdditionalFields["usePgpass"] = "true"
1052-
} else {
1053-
finalAdditionalFields.removeValue(forKey: "usePgpass")
1054-
}
10551072
let trimmedScript = preConnectScript.trimmingCharacters(in: .whitespacesAndNewlines)
10561073
if !trimmedScript.isEmpty {
10571074
finalAdditionalFields["preConnectScript"] = preConnectScript
@@ -1071,9 +1088,7 @@ struct ConnectionFormView: View {
10711088
color: connectionColor,
10721089
tagId: selectedTagId,
10731090
groupId: selectedGroupId,
1074-
redisDatabase: type.pluginTypeId == "Redis"
1075-
? Int(additionalFieldValues["redisDatabase"] ?? "0")
1076-
: nil,
1091+
redisDatabase: Int(additionalFieldValues["redisDatabase"] ?? ""),
10771092
startupCommands: startupCommands.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
10781093
? nil : startupCommands,
10791094
additionalFields: finalAdditionalFields.isEmpty ? nil : finalAdditionalFields

0 commit comments

Comments
 (0)