Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- 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
- 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
- Replaced `AppState.isMongoDB`/`isRedis` booleans with `AppState.editorLanguage: EditorLanguage` for extensible editor language detection
- Theme colors now derived from plugin `brandColorHex` instead of hardcoded `Theme.mysqlColor` etc.
- Sidebar labels ("Tables"/"Collections"/"Keys"), toolbar preview labels, and AI prompt language detection now use plugin metadata
Expand Down
11 changes: 10 additions & 1 deletion Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,16 @@ final class PostgreSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
static let databaseDisplayName = "PostgreSQL"
static let iconName = "cylinder.fill"
static let defaultPort = 5432
static let additionalConnectionFields: [ConnectionField] = []
static let additionalConnectionFields: [ConnectionField] = [
ConnectionField(
id: "usePgpass",
label: String(localized: "Use ~/.pgpass"),
defaultValue: "false",
fieldType: .toggle,
section: .authentication,
hidesPassword: true
)
]
static let additionalDatabaseTypeIds: [String] = ["Redshift"]

// MARK: - UI/Capability Metadata
Expand Down
2 changes: 2 additions & 0 deletions Plugins/SQLiteDriverPlugin/SQLitePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ final class SQLitePlugin: NSObject, TableProPlugin, DriverPlugin {
// MARK: - UI/Capability Metadata

static let requiresAuthentication = false
static let supportsSSH = false
static let supportsSSL = false
static let connectionMode: ConnectionMode = .fileBased
static let urlSchemes: [String] = ["sqlite"]
static let fileExtensions: [String] = ["db", "sqlite", "sqlite3"]
Expand Down
29 changes: 28 additions & 1 deletion Plugins/TableProPluginKit/ConnectionField.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import Foundation

public enum FieldSection: String, Codable, Sendable {
case authentication
case advanced
}

public struct ConnectionField: Codable, Sendable {
public struct IntRange: Codable, Sendable, Equatable {
public let lowerBound: Int
Expand Down Expand Up @@ -70,6 +75,8 @@ public struct ConnectionField: Codable, Sendable {
public let isRequired: Bool
public let defaultValue: String?
public let fieldType: FieldType
public let section: FieldSection
public let hidesPassword: Bool

/// Backward-compatible convenience: true when fieldType is .secure
public var isSecure: Bool {
Expand All @@ -84,13 +91,33 @@ public struct ConnectionField: Codable, Sendable {
required: Bool = false,
secure: Bool = false,
defaultValue: String? = nil,
fieldType: FieldType? = nil
fieldType: FieldType? = nil,
section: FieldSection = .advanced,
hidesPassword: Bool = false
) {
self.id = id
self.label = label
self.placeholder = placeholder
self.isRequired = required
self.defaultValue = defaultValue
self.fieldType = fieldType ?? (secure ? .secure : .text)
self.section = section
self.hidesPassword = hidesPassword
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
label = try container.decode(String.self, forKey: .label)
placeholder = try container.decodeIfPresent(String.self, forKey: .placeholder) ?? ""
isRequired = try container.decodeIfPresent(Bool.self, forKey: .isRequired) ?? false
defaultValue = try container.decodeIfPresent(String.self, forKey: .defaultValue)
fieldType = try container.decode(FieldType.self, forKey: .fieldType)
section = try container.decodeIfPresent(FieldSection.self, forKey: .section) ?? .advanced
hidesPassword = try container.decodeIfPresent(Bool.self, forKey: .hidesPassword) ?? false
}

private enum CodingKeys: String, CodingKey {
case id, label, placeholder, isRequired, defaultValue, fieldType, section, hidesPassword
}
}
4 changes: 4 additions & 0 deletions Plugins/TableProPluginKit/DriverPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public protocol DriverPlugin: TableProPlugin {
static var structureColumnFields: [StructureColumnField] { get }
static var defaultPrimaryKeyColumn: String? { get }
static var supportsQueryProgress: Bool { get }
static var supportsSSH: Bool { get }
static var supportsSSL: Bool { get }
}

public extension DriverPlugin {
Expand Down Expand Up @@ -98,4 +100,6 @@ public extension DriverPlugin {
}
static var defaultPrimaryKeyColumn: String? { nil }
static var supportsQueryProgress: Bool { false }
static var supportsSSH: Bool { true }
static var supportsSSL: Bool { true }
}
4 changes: 1 addition & 3 deletions TablePro/Core/Database/DatabaseDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -352,9 +352,7 @@ enum DatabaseDriverFactory {
}

private static func resolvePassword(for connection: DatabaseConnection) -> String {
if connection.usePgpass
&& (connection.type == .postgresql || connection.type == .redshift)
{
if connection.usePgpass {
return ""
}
return ConnectionStorage.shared.loadPassword(for: connection.id) ?? ""
Expand Down
10 changes: 10 additions & 0 deletions TablePro/Core/Plugins/PluginManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,16 @@ final class PluginManager {
return Swift.type(of: plugin).supportsQueryProgress
}

func supportsSSH(for databaseType: DatabaseType) -> Bool {
guard let plugin = driverPlugin(for: databaseType) else { return true }
return Swift.type(of: plugin).supportsSSH
}

func supportsSSL(for databaseType: DatabaseType) -> Bool {
guard let plugin = driverPlugin(for: databaseType) else { return true }
return Swift.type(of: plugin).supportsSSL
}

func autoLimitStyle(for databaseType: DatabaseType) -> AutoLimitStyle {
guard let plugin = driverPlugin(for: databaseType) else { return .limit }
guard let dialect = Swift.type(of: plugin).sqlDialect else { return .none }
Expand Down
81 changes: 44 additions & 37 deletions TablePro/Views/Connection/ConnectionFormView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ struct ConnectionFormView: View {
PluginManager.shared.additionalConnectionFields(for: type)
}

private var authSectionFields: [ConnectionField] {
PluginManager.shared.additionalConnectionFields(for: type)
.filter { $0.section == .authentication }
}

private var hidePasswordField: Bool {
authSectionFields.contains { $0.hidesPassword && additionalFieldValues[$0.id] == "true" }
}

@State private var name: String = ""
@State private var host: String = ""
@State private var port: String = ""
Expand Down Expand Up @@ -83,9 +92,12 @@ struct ConnectionFormView: View {
@State private var startupCommands: String = ""

// Pgpass
@State private var usePgpass: Bool = false
@State private var pgpassStatus: PgpassStatus = .notChecked

private var usePgpass: Bool {
additionalFieldValues["usePgpass"] == "true"
}

// Pre-connect script
@State private var preConnectScript: String = ""

Expand Down Expand Up @@ -143,14 +155,10 @@ struct ConnectionFormView: View {
if hasLoadedData {
port = String(newType.defaultPort)
}
let isFileBased = PluginManager.shared.connectionMode(for: newType) == .fileBased
if isFileBased && (selectedTab == .ssh || selectedTab == .ssl) {
if !visibleTabs.contains(selectedTab) {
selectedTab = .general
}
additionalFieldValues = [:]
if newType.pluginTypeId != "PostgreSQL" {
usePgpass = false
}
for field in PluginManager.shared.additionalConnectionFields(for: newType) {
if let defaultValue = field.defaultValue {
additionalFieldValues[field.id] = defaultValue
Expand All @@ -160,7 +168,7 @@ struct ConnectionFormView: View {
.pluginInstallPrompt(connection: $pluginInstallConnection) { connection in
connectAfterInstall(connection)
}
.onChange(of: usePgpass) { _, _ in updatePgpassStatus() }
.onChange(of: additionalFieldValues) { _, _ in updatePgpassStatus() }
.onChange(of: host) { _, _ in updatePgpassStatus() }
.onChange(of: port) { _, _ in updatePgpassStatus() }
.onChange(of: database) { _, _ in updatePgpassStatus() }
Expand All @@ -170,10 +178,15 @@ struct ConnectionFormView: View {
// MARK: - Tab Picker Helpers

private var visibleTabs: [FormTab] {
if PluginManager.shared.connectionMode(for: type) == .fileBased {
return [.general, .advanced]
var tabs: [FormTab] = [.general]
if PluginManager.shared.supportsSSH(for: type) {
tabs.append(.ssh)
}
if PluginManager.shared.supportsSSL(for: type) {
tabs.append(.ssl)
}
return FormTab.allCases
tabs.append(.advanced)
return tabs
}

private var resolvedSSHAgentSocketPath: String {
Expand Down Expand Up @@ -273,16 +286,25 @@ struct ConnectionFormView: View {
prompt: Text("root")
)
}
if type.pluginTypeId == "PostgreSQL" {
Toggle(String(localized: "Use ~/.pgpass"), isOn: $usePgpass)
ForEach(authSectionFields, id: \.id) { field in
ConnectionFieldRow(
field: field,
value: Binding(
get: {
additionalFieldValues[field.id]
?? field.defaultValue ?? ""
},
set: { additionalFieldValues[field.id] = $0 }
)
)
}
if !usePgpass || type.pluginTypeId != "PostgreSQL" {
if !hidePasswordField {
SecureField(
String(localized: "Password"),
text: $password
)
}
if usePgpass && type.pluginTypeId == "PostgreSQL" {
if additionalFieldValues["usePgpass"] == "true" {
pgpassStatusView
}
}
Expand Down Expand Up @@ -628,9 +650,10 @@ struct ConnectionFormView: View {

private var advancedForm: some View {
Form {
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(
field: field,
value: Binding(
Expand Down Expand Up @@ -775,7 +798,7 @@ struct ConnectionFormView: View {
}

private func updatePgpassStatus() {
guard usePgpass, type.pluginTypeId == "PostgreSQL" else {
guard additionalFieldValues["usePgpass"] == "true" else {
pgpassStatus = .notChecked
return
}
Expand Down Expand Up @@ -826,9 +849,8 @@ struct ConnectionFormView: View {
// Load additional fields from connection
additionalFieldValues = existing.additionalFields

// Migrate legacy Redis database index before default seeding
if existing.type.pluginTypeId == "Redis",
additionalFieldValues["redisDatabase"] == nil,
// Migrate legacy redisDatabase to additionalFields
if additionalFieldValues["redisDatabase"] == nil,
let rdb = existing.redisDatabase {
additionalFieldValues["redisDatabase"] = String(rdb)
}
Expand All @@ -841,7 +863,6 @@ struct ConnectionFormView: View {

// Load startup commands
startupCommands = existing.startupCommands ?? ""
usePgpass = existing.usePgpass
preConnectScript = existing.preConnectScript ?? ""

// Load passwords from Keychain
Expand Down Expand Up @@ -888,11 +909,6 @@ struct ConnectionFormView: View {
? "root" : trimmedUsername

var finalAdditionalFields = additionalFieldValues
if usePgpass && type.pluginTypeId == "PostgreSQL" {
finalAdditionalFields["usePgpass"] = "true"
} else {
finalAdditionalFields.removeValue(forKey: "usePgpass")
}
let trimmedScript = preConnectScript.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedScript.isEmpty {
finalAdditionalFields["preConnectScript"] = preConnectScript
Expand All @@ -915,9 +931,7 @@ struct ConnectionFormView: View {
groupId: selectedGroupId,
safeModeLevel: safeModeLevel,
aiPolicy: aiPolicy,
redisDatabase: type.pluginTypeId == "Redis"
? Int(additionalFieldValues["redisDatabase"] ?? "0")
: nil,
redisDatabase: additionalFieldValues["redisDatabase"].map { Int($0) ?? 0 },
startupCommands: startupCommands.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
? nil : startupCommands,
additionalFields: finalAdditionalFields.isEmpty ? nil : finalAdditionalFields
Expand Down Expand Up @@ -1047,11 +1061,6 @@ struct ConnectionFormView: View {
? "root" : trimmedUsername

var finalAdditionalFields = additionalFieldValues
if usePgpass && type.pluginTypeId == "PostgreSQL" {
finalAdditionalFields["usePgpass"] = "true"
} else {
finalAdditionalFields.removeValue(forKey: "usePgpass")
}
let trimmedScript = preConnectScript.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedScript.isEmpty {
finalAdditionalFields["preConnectScript"] = preConnectScript
Expand All @@ -1071,9 +1080,7 @@ struct ConnectionFormView: View {
color: connectionColor,
tagId: selectedTagId,
groupId: selectedGroupId,
redisDatabase: type.pluginTypeId == "Redis"
? Int(additionalFieldValues["redisDatabase"] ?? "0")
: nil,
redisDatabase: additionalFieldValues["redisDatabase"].map { Int($0) ?? 0 },
startupCommands: startupCommands.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
? nil : startupCommands,
additionalFields: finalAdditionalFields.isEmpty ? nil : finalAdditionalFields
Expand Down
Loading