fix(connections): show mobile Vault status for selected workspace
This commit is contained in:
1 parent
6d8bd1dd6b
commit
6140bc3ebe
7 files changed
+65
-21
No files matched your search
@@ -1009,11 +1009,10 @@ public final class HostLibraryModel: RemoteSessionHostStore {
|
||||
statuses[id] = status
|
||||
}
|
||||
|
||||
/// Records one live connection's state and projects terminals and File
|
||||
/// Transfer browsers for the same host into the status dot shown by Hosts
|
||||
/// and host pickers. A connected source wins over a connecting or failed
|
||||
/// sibling, so closing one pane cannot make an otherwise-online host appear
|
||||
/// offline.
|
||||
/// Records one live connection's state for Host-level consumers, including
|
||||
/// the Vault and Recents dots and the agent Host listing. A connected source
|
||||
/// wins over a connecting or failed sibling, so closing one pane cannot make
|
||||
/// an otherwise-online Host appear offline.
|
||||
public func setConnectionStatus(
|
||||
_ status: ConnectionStatus, connectionID: UUID, hostID: Host.ID
|
||||
) {
|
||||
|
||||
@@ -37,7 +37,7 @@ final class MobileSceneModel {
|
||||
@ObservationIgnored private var pendingWorkspaceID: MobileWorkspace.ID?
|
||||
@ObservationIgnored private var pendingBatchStartup:
|
||||
(hostIDs: [Host.ID], connectionProtocol: MobileConnectionProtocol)?
|
||||
@ObservationIgnored private var recentWorkspaceIDs: [MobileWorkspace.ID] = []
|
||||
private var recentWorkspaceIDs: [MobileWorkspace.ID] = []
|
||||
|
||||
init(app: MobileAppModel, sessionID: String) {
|
||||
workspaceSession = app.workspaceSession(for: sessionID)
|
||||
@@ -179,21 +179,28 @@ final class MobileSceneModel {
|
||||
return
|
||||
}
|
||||
let kind: ConnectionHistoryEntry.Kind = connectionProtocol == .ssh ? .ssh : .telnet
|
||||
guard let workspace = workspaceActivatedByHostRow(for: host) else {
|
||||
open(hostID: hostID, kind: kind)
|
||||
return
|
||||
}
|
||||
selectWorkspace(workspace.id)
|
||||
}
|
||||
|
||||
/// The connection a Vault Host card will reopen in this window.
|
||||
func workspaceActivatedByHostRow(for host: Host) -> MobileWorkspace? {
|
||||
guard let connectionProtocol = host.preferredTerminalProtocol else { return nil }
|
||||
let kind: ConnectionHistoryEntry.Kind = connectionProtocol == .ssh ? .ssh : .telnet
|
||||
let matchingIDs = Set(
|
||||
workspaceSession.workspaces.lazy
|
||||
.filter { $0.descriptor.hostID == hostID && $0.descriptor.kind == kind }
|
||||
.filter { $0.descriptor.hostID == host.id && $0.descriptor.kind == kind }
|
||||
.map(\.id)
|
||||
)
|
||||
let workspaceID =
|
||||
recentWorkspaceIDs.first(where: matchingIDs.contains)
|
||||
?? workspaceSession.workspaces.last(where: {
|
||||
$0.descriptor.hostID == hostID && $0.descriptor.kind == kind
|
||||
$0.descriptor.hostID == host.id && $0.descriptor.kind == kind
|
||||
})?.id
|
||||
guard let workspaceID else {
|
||||
open(hostID: hostID, kind: kind)
|
||||
return
|
||||
}
|
||||
selectWorkspace(workspaceID)
|
||||
return workspaceID.flatMap { id in workspaceSession.workspaces.first { $0.id == id } }
|
||||
}
|
||||
|
||||
func openSameDirectory(_ launch: MobileWorkspaceDirectoryLaunch) {
|
||||
|
||||
@@ -784,8 +784,22 @@ struct MobileVaultView: View {
|
||||
} content: {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
HostSystemIcon(asset: entry?.asset, size: 36)
|
||||
.tint(tint)
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
HostSystemIcon(asset: entry?.asset, size: 36)
|
||||
.tint(tint)
|
||||
if let workspace = reviewScene?.workspaceActivatedByHostRow(for: host),
|
||||
let indicator = MobileWorkspaceConnectionIndicator.resolve(
|
||||
connected: workspace.connected,
|
||||
busy: workspace.busy,
|
||||
reconnecting: workspace.reconnectState != nil
|
||||
)
|
||||
{
|
||||
Circle()
|
||||
.fill(indicator.tint)
|
||||
.frame(width: 8, height: 8)
|
||||
.overlay { Circle().stroke(Color(uiColor: .systemBackground), lineWidth: 1.5) }
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(host.name).font(.headline).foregroundStyle(.primary).lineLimit(1)
|
||||
Text(app.library.preferredTerminalLoginAddress(for: host)).font(.caption.monospaced())
|
||||
|
||||
@@ -171,6 +171,32 @@ struct MobileWorkspaceSessionTests {
|
||||
#expect(second.isWorkspaceUnavailable)
|
||||
}
|
||||
|
||||
@Test func vaultHostIndicatorFollowsTheWorkspaceOpenedByItsCard() throws {
|
||||
let fixture = try Fixture()
|
||||
defer { fixture.cleanUp() }
|
||||
let app = try fixture.makeApp()
|
||||
let scene = MobileSceneModel(app: app, sessionID: "vault-card")
|
||||
let other = MobileSceneModel(app: app, sessionID: "other-window")
|
||||
let host = fixture.host
|
||||
|
||||
other.open(hostID: host.id, kind: .ssh)
|
||||
#expect(scene.workspaceActivatedByHostRow(for: host) == nil)
|
||||
|
||||
scene.open(hostID: host.id, kind: .sftp)
|
||||
#expect(scene.workspaceActivatedByHostRow(for: host) == nil)
|
||||
|
||||
scene.open(hostID: host.id, kind: .ssh)
|
||||
let first = try #require(scene.workspaceActivatedByHostRow(for: host))
|
||||
scene.open(hostID: host.id, kind: .ssh)
|
||||
let second = try #require(scene.workspaceActivatedByHostRow(for: host))
|
||||
#expect(first !== second)
|
||||
|
||||
scene.selectWorkspace(first.id)
|
||||
#expect(scene.workspaceActivatedByHostRow(for: host) === first)
|
||||
scene.activateHost(host.id)
|
||||
#expect(scene.activeWorkspace === first)
|
||||
}
|
||||
|
||||
@Test func everyCreationEntryPointUsesTheReceivingWindow() throws {
|
||||
let fixture = try Fixture()
|
||||
defer { fixture.cleanUp() }
|
||||
|
||||
@@ -1035,12 +1035,11 @@ final class AppModel {
|
||||
return !unsafeExtensions.contains((file.name as NSString).pathExtension.lowercased())
|
||||
}
|
||||
|
||||
/// Mark as recently used, and flip the status dot.
|
||||
/// Optimistically shows connecting until the session reports its own state.
|
||||
///
|
||||
/// Its own method because three code paths now open hosts — one, a batch of
|
||||
/// workspaces, and a batch into one Workspace — and *where it lands* is the only
|
||||
/// thing that should differ between them. Inlining this was fine while there
|
||||
/// were two.
|
||||
/// thing that should differ between them.
|
||||
private func beginOpening(_ host: Host, as target: HostOpenTarget) {
|
||||
switch target {
|
||||
case .preferredTerminal, .sshSession, .telnetSession:
|
||||
|
||||
@@ -237,8 +237,8 @@ extension HostLibraryModel {
|
||||
knownHosts: knownHosts
|
||||
)
|
||||
|
||||
// A spread of connection states so the sidebar, workspace list, and status bar
|
||||
// can be checked against every dot color without a server.
|
||||
// A spread of connection states so Vault and Welcome Host cards can be
|
||||
// checked against every dot color without a server.
|
||||
store.setStatus(.connected, for: web01.id)
|
||||
store.setStatus(.connecting, for: db.id)
|
||||
if let staging = hosts.first(where: { $0.name == "staging-app" }) {
|
||||
|
||||
@@ -231,7 +231,6 @@ struct StatusBarView: View {
|
||||
sshSession?.status
|
||||
?? sftpChannel?.status
|
||||
?? serialSession?.status
|
||||
?? pane?.hostID.map { app.hostLibrary.status(of: $0) }
|
||||
?? .disconnected
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user