diff --git a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift index d13be3c6..83ff709a 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift @@ -141,7 +141,6 @@ enum AgentStatus: Int, Equatable, Comparable { struct Workspace: Identifiable, Equatable, Comparable { let id: UUID let name: String - var agents: Set static func < (lhs: Workspace, rhs: Workspace) -> Bool { lhs.name.localizedCompare(rhs.name) == .orderedAscending @@ -156,19 +155,18 @@ struct VPNMenuState { var invalidAgents: [Vpn_Agent] = [] public func findAgent(workspaceID: UUID, name: String) -> Agent? { - agents.first(where: { $0.value.wsID == workspaceID && $0.value.name == name })?.value + agents.values.first { $0.wsID == workspaceID && $0.name.caseInsensitiveCompare(name) == .orderedSame } } public func findWorkspace(name: String) -> Workspace? { - workspaces - .first(where: { $0.value.name == name })?.value + workspaces.values.first { $0.name.caseInsensitiveCompare(name) == .orderedSame } } mutating func upsertAgent(_ agent: Vpn_Agent) { guard let id = UUID(uuidData: agent.id), let wsID = UUID(uuidData: agent.workspaceID), - var workspace = workspaces[wsID], + let workspace = workspaces[wsID], !agent.fqdn.isEmpty else { invalidAgents.append(agent) @@ -179,10 +177,8 @@ struct VPNMenuState { // An existing agent with the same name, belonging to the same workspace // is from a previous workspace build, and should be removed. - agents.filter { $0.value.name == agent.name && $0.value.wsID == wsID } + agents.filter { $0.value.name.caseInsensitiveCompare(agent.name) == .orderedSame && $0.value.wsID == wsID } .forEach { agents[$0.key] = nil } - workspace.agents.insert(id) - workspaces[wsID] = workspace var lastPing: LastPing? if agent.hasLastPing { @@ -212,11 +208,6 @@ struct VPNMenuState { mutating func deleteAgent(withId id: Data) { guard let agentUUID = UUID(uuidData: id) else { return } - // Update Workspaces - if let agent = agents[agentUUID], var ws = workspaces[agent.wsID] { - ws.agents.remove(agentUUID) - workspaces[agent.wsID] = ws - } agents[agentUUID] = nil // Remove from invalid agents if present invalidAgents.removeAll { invalidAgent in @@ -226,9 +217,7 @@ struct VPNMenuState { mutating func upsertWorkspace(_ workspace: Vpn_Workspace) { guard let wsID = UUID(uuidData: workspace.id) else { return } - // Workspace names are unique & case-insensitive, and we want to show offline workspaces - // with a valid hostname (lowercase). - workspaces[wsID] = Workspace(id: wsID, name: workspace.name.lowercased(), agents: []) + workspaces[wsID] = Workspace(id: wsID, name: workspace.name) // Check if we can associate any invalid agents with this workspace invalidAgents.filter { agent in agent.workspaceID == workspace.id @@ -251,9 +240,10 @@ struct VPNMenuState { var sorted: [VPNMenuItem] { var items = agents.values.map { VPNMenuItem.agent($0) } // Workspaces with no agents are shown as offline - items += workspaces.filter { _, value in - value.agents.isEmpty - }.map { VPNMenuItem.offlineWorkspace(Workspace(id: $0.key, name: $0.value.name, agents: $0.value.agents)) } + let workspacesWithAgents = Set(agents.values.map(\.wsID)) + items += workspaces.values + .filter { !workspacesWithAgents.contains($0.id) } + .map { VPNMenuItem.offlineWorkspace($0) } return items.sorted() } diff --git a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift index 71e753d2..c598889d 100644 --- a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift +++ b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift @@ -45,7 +45,7 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable { func primaryHost(hostnameSuffix: String) -> String { switch self { case let .agent(agent): agent.primaryHost - case .offlineWorkspace: "\(wsName).\(hostnameSuffix)" + case .offlineWorkspace: "\(wsName.lowercased()).\(hostnameSuffix)" } } diff --git a/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift b/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift index 8f84ab3d..fae433ca 100644 --- a/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift @@ -109,7 +109,7 @@ struct AgentsTests { vpn.state = .connected vpn.menuState = .init( agents: createMockAgents(count: Theme.defaultVisibleAgents - 1), - workspaces: [UUID(): Workspace(id: UUID(), name: "offline", agents: .init())] + workspaces: [UUID(): Workspace(id: UUID(), name: "offline")] ) try await ViewHosting.host(view) { diff --git a/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift b/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift index dbd61a93..dd9acf52 100644 --- a/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift @@ -162,7 +162,7 @@ struct VPNMenuStateTests { let newAgent = Vpn_Agent.with { $0.id = newAgentID.uuidData $0.workspaceID = workspaceID.uuidData - $0.name = "agent1" // Same name as old agent + $0.name = "Agent1" // Same name as old agent, different case $0.lastHandshake = .init(date: Date.now) $0.lastPing = .with { $0.latency = .init(floatLiteral: 0.05) @@ -174,24 +174,28 @@ struct VPNMenuStateTests { #expect(state.agents[oldAgentID] == nil) let storedAgent = try #require(state.agents[newAgentID]) - #expect(storedAgent.name == "agent1") + #expect(storedAgent.name == "Agent1") #expect(storedAgent.wsID == workspaceID) #expect(storedAgent.primaryHost == "foo.coder") #expect(storedAgent.status == .okay) + + state.deleteAgent(withId: newAgentID.uuidData) + #expect(state.sorted.map(\.id) == [workspaceID]) } @Test mutating func testUpsertWorkspace_addsOfflineWorkspace() async throws { let workspaceID = UUID() - state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "foo" }) + state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "Foo" }) let storedWorkspace = try #require(state.workspaces[workspaceID]) - #expect(storedWorkspace.name == "foo") + #expect(storedWorkspace.name == "Foo") var output = state.sorted #expect(output.count == 1) #expect(output[0].id == workspaceID) - #expect(output[0].wsName == "foo") + #expect(output[0].wsName == "Foo") + #expect(output[0].primaryHost(hostnameSuffix: "coder") == "foo.coder") let agentID = UUID() let agent = Vpn_Agent.with { @@ -210,12 +214,32 @@ struct VPNMenuStateTests { output = state.sorted #expect(output.count == 1) #expect(output[0].id == agentID) - #expect(output[0].wsName == "foo") + #expect(output[0].wsName == "Foo") #expect(output[0].status == .okay) let storedAgentFromSort = try #require(state.agents[agentID]) #expect(storedAgentFromSort.statusString.contains("You're connected through a DERP relay.")) #expect(storedAgentFromSort.statusString.contains("Total latency: 50.00 ms")) #expect(storedAgentFromSort.statusString.contains("Last handshake: 3 minutes ago")) + + state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "Foo" }) + #expect(state.sorted.map(\.id) == [agentID]) + } + + @Test + mutating func testFind_ignoresNameCase() async throws { + let workspaceID = UUID() + let agentID = UUID() + state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "My-Workspace" }) + state.upsertAgent(Vpn_Agent.with { + $0.id = agentID.uuidData + $0.workspaceID = workspaceID.uuidData + $0.name = "Main" + $0.fqdn = ["main.my-workspace.me.coder"] + }) + + #expect(state.findWorkspace(name: "MY-WORKSPACE")?.id == workspaceID) + #expect(state.findAgent(workspaceID: workspaceID, name: "MAIN")?.id == agentID) + #expect(state.findWorkspace(name: "other") == nil) } @Test