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 @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Connection list rows show the database name after the host, so connections that share a name and host are easier to tell apart. (#1535)
- Save as Favorite uses Cmd+D again. The Cmd+Control+D set in 0.47.0 is reserved by macOS for Look Up, so it never fired.
- Editor toolbar buttons show their keyboard shortcut in the tooltip, and it updates if you rebind the shortcut.

Expand Down
59 changes: 59 additions & 0 deletions TablePro/Models/Connection/DatabaseConnection+Display.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// DatabaseConnection+Display.swift
// TablePro
//

import Foundation
import TableProPluginKit

extension DatabaseConnection {
var connectionSubtitle: String {
var components: [String] = [endpointDescription]
if let database = databaseDescriptor {
components.append(database)
}
if let via = sshViaDescriptor {
components.append(via)
}
return components.joined(separator: " · ")
}

private var endpointDescription: String {
if host.isEmpty {
let trimmed = database.trimmingCharacters(in: .whitespaces)
return trimmed.isEmpty ? type.rawValue : (trimmed as NSString).abbreviatingWithTildeInPath
}
if host.hasPrefix("/") {
return (host as NSString).abbreviatingWithTildeInPath
}
if let mongoHosts = additionalFields["mongoHosts"], mongoHosts.contains(",") {
let count = mongoHosts.split(separator: ",").count
return String(format: String(localized: "%@ (+%d more)"), hostWithOptionalPort, count - 1)
}
return hostWithOptionalPort
}

private var databaseDescriptor: String? {
guard !host.isEmpty else { return nil }
switch type.pathFieldRole {
case .database, .serviceName:
let trimmed = database.trimmingCharacters(in: .whitespaces)
return trimmed.isEmpty ? nil : trimmed
Comment on lines +39 to +41
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the Oracle service field in subtitles

For Oracle connections created through the form or URL import, the service name is stored in connection.oracleServiceName / additionalFields["oracleServiceName"] and database can remain empty; the Oracle driver also connects using that additional field. Treating .serviceName the same as .database here means those rows still render only the host, so two Oracle connections to different services on the same host remain indistinguishable despite the new feature.

Useful? React with 👍 / 👎.

case .databaseIndex:
guard let index = redisDatabase else { return nil }
return String(format: String(localized: "db %d"), index)
case .filePath:
return nil
}
}

private var hostWithOptionalPort: String {
port == type.defaultPort ? host : "\(host):\(port)"
}

private var sshViaDescriptor: String? {
let ssh = resolvedSSHConfig
guard ssh.enabled, !ssh.host.isEmpty else { return nil }
return String(format: String(localized: "via %@"), ssh.host)
}
}
4 changes: 4 additions & 0 deletions TablePro/Models/Connection/DatabaseConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ extension DatabaseType {
PluginMetadataRegistry.shared.snapshot(forTypeId: rawValue)?.connection.category ?? .other
}

var pathFieldRole: PathFieldRole {
PluginMetadataRegistry.shared.snapshot(forTypeId: rawValue)?.pathFieldRole ?? .database
}

var tagline: String? {
let raw = PluginMetadataRegistry.shared.snapshot(forTypeId: rawValue)?.connection.tagline ?? ""
return raw.isEmpty ? nil : raw
Expand Down
39 changes: 2 additions & 37 deletions TablePro/Views/Connection/WelcomeConnectionRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ struct WelcomeConnectionRow: View {
.font(.body)
.foregroundStyle(.primary)

Text(subtitleText)
Text(connection.connectionSubtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
.help(subtitleText)
.help(connection.connectionSubtitle)
}

Spacer(minLength: 8)
Expand Down Expand Up @@ -131,39 +131,4 @@ struct WelcomeConnectionRow: View {
.foregroundStyle(.tertiary)
}
}

private var subtitleText: String {
var components: [String] = [primaryEndpoint]
if let viaText = sshViaText {
components.append(viaText)
}
return components.joined(separator: " · ")
}

private var primaryEndpoint: String {
if connection.host.isEmpty {
return connection.database.isEmpty ? connection.type.rawValue : connection.database
}
if connection.host.hasPrefix("/") {
return (connection.host as NSString).abbreviatingWithTildeInPath
}
if let mongoHosts = connection.additionalFields["mongoHosts"], mongoHosts.contains(",") {
let count = mongoHosts.split(separator: ",").count
return String(format: String(localized: "%@ (+%d more)"), hostWithOptionalPort, count - 1)
}
return hostWithOptionalPort
}

private var hostWithOptionalPort: String {
if connection.port == connection.type.defaultPort {
return connection.host
}
return "\(connection.host):\(connection.port)"
}

private var sshViaText: String? {
let ssh = connection.resolvedSSHConfig
guard ssh.enabled, !ssh.host.isEmpty else { return nil }
return String(format: String(localized: "via %@"), ssh.host)
}
}
169 changes: 169 additions & 0 deletions TableProTests/Models/DatabaseConnectionDisplayTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//
// DatabaseConnectionDisplayTests.swift
// TableProTests
//

import Foundation
import TableProPluginKit
import Testing

@testable import TablePro

@Suite("DatabaseConnection display")
struct DatabaseConnectionDisplayTests {
@Test("Relational connection shows database after host")
func relationalShowsDatabase() {
let connection = DatabaseConnection(
name: "Project", host: "localhost", port: 3_306,
database: "myapp_production", type: .mysql
)

#expect(connection.connectionSubtitle == "localhost · myapp_production")
}

@Test("Non-default port is shown before the database")
func nonDefaultPortShown() {
let connection = DatabaseConnection(
name: "Project", host: "localhost", port: 3_307,
database: "myapp_production", type: .mysql
)

#expect(connection.connectionSubtitle == "localhost:3307 · myapp_production")
}

@Test("Empty database leaves no trailing separator")
func emptyDatabaseHasNoSeparator() {
let connection = DatabaseConnection(
name: "Project", host: "localhost", port: 3_306,
database: "", type: .mysql
)

#expect(connection.connectionSubtitle == "localhost")
}

@Test("PostgreSQL shows database on its default port")
func postgresShowsDatabase() {
let connection = DatabaseConnection(
name: "Analytics", host: "db.example.com", port: 5_432,
database: "analytics", type: .postgresql
)

#expect(connection.connectionSubtitle == "db.example.com · analytics")
}

@Test("Two same-named, same-host connections differ by database")
func sameNameSameHostDifferByDatabase() {
let staging = DatabaseConnection(
name: "Acme", host: "10.0.0.5", port: 5_432, database: "acme_staging", type: .postgresql
)
let production = DatabaseConnection(
name: "Acme", host: "10.0.0.5", port: 5_432, database: "acme_production", type: .postgresql
)

#expect(staging.connectionSubtitle != production.connectionSubtitle)
}

@Test("File-based connection shows the path without duplicating it")
func fileBasedShowsPathOnce() {
let connection = DatabaseConnection(
name: "Local", host: "", port: 0,
database: "/var/db/app.sqlite", type: .sqlite
)

#expect(connection.connectionSubtitle == "/var/db/app.sqlite")
}

@Test("File-based connection abbreviates a home-relative path with a tilde")
func fileBasedAbbreviatesHomePath() {
let path = (NSHomeDirectory() as NSString).appendingPathComponent("databases/app.sqlite")
let connection = DatabaseConnection(
name: "Local", host: "", port: 0, database: path, type: .sqlite
)

#expect(connection.connectionSubtitle == "~/databases/app.sqlite")
}

@Test("Unix socket host is abbreviated and keeps the database segment")
func unixSocketHostAbbreviatesAndKeepsDatabase() {
let socket = (NSHomeDirectory() as NSString).appendingPathComponent("run/mysql.sock")
let connection = DatabaseConnection(
name: "Project", host: socket, port: 3_306,
database: "appdb", type: .mysql
)

#expect(connection.connectionSubtitle == "~/run/mysql.sock · appdb")
}

@Test("File-based connection with no path falls back to the type name")
func fileBasedEmptyFallsBackToType() {
let connection = DatabaseConnection(
name: "In-memory", host: "", port: 0, database: "", type: .duckdb
)

#expect(connection.connectionSubtitle == "DuckDB")
}

@Test("Redis shows the database index when set")
func redisShowsIndex() {
let connection = DatabaseConnection(
name: "Cache", host: "localhost", port: 6_379,
database: "", type: .redis, redisDatabase: 3
)

#expect(connection.connectionSubtitle == "localhost · db 3")
}

@Test("Redis without an index shows only the host")
func redisWithoutIndexShowsHost() {
let connection = DatabaseConnection(
name: "Cache", host: "localhost", port: 6_379,
database: "", type: .redis, redisDatabase: nil
)

#expect(connection.connectionSubtitle == "localhost")
}

@Test("Oracle shows the service name after the host")
func oracleShowsServiceName() {
let connection = DatabaseConnection(
name: "ERP", host: "ora.example.com", port: 1_521,
database: "ORCLPDB1", type: .oracle
)

#expect(connection.connectionSubtitle == "ora.example.com · ORCLPDB1")
}

@Test("MongoDB replica set shows host count and database")
func mongoReplicaSetShowsCountAndDatabase() {
let connection = DatabaseConnection(
name: "Docs", host: "node1.example.com", port: 27_017,
database: "appdb", type: .mongodb,
additionalFields: ["mongoHosts": "node1.example.com,node2.example.com,node3.example.com"]
)

#expect(connection.connectionSubtitle == "node1.example.com (+2 more) · appdb")
}

@Test("SSH via segment comes last")
func sshViaComesLast() {
var sshConfig = SSHConfiguration()
sshConfig.enabled = true
sshConfig.host = "bastion.example.com"
let connection = DatabaseConnection(
name: "Project", host: "localhost", port: 3_306,
database: "myapp", type: .mysql, sshConfig: sshConfig
)

#expect(connection.connectionSubtitle == "localhost · myapp · via bastion.example.com")
}

@Test("Unknown future type is treated like a database role")
func unknownTypeUsesDatabaseRole() {
let connection = DatabaseConnection(
name: "Future", host: "future.example.com", port: 0,
database: "mydb", type: DatabaseType(rawValue: "FutureDB")
)

#expect(connection.connectionSubtitle == "future.example.com · mydb")
}
}
Loading