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
28 changes: 9 additions & 19 deletions Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ enum AgentStatus: Int, Equatable, Comparable {
struct Workspace: Identifiable, Equatable, Comparable {
let id: UUID
let name: String
var agents: Set<UUID>

static func < (lhs: Workspace, rhs: Workspace) -> Bool {
lhs.name.localizedCompare(rhs.name) == .orderedAscending
Expand All @@ -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)
Expand All @@ -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 }
Comment thread
ethanndickson marked this conversation as resolved.
workspace.agents.insert(id)
workspaces[wsID] = workspace

var lastPing: LastPing?
if agent.hasLastPing {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()
}

Expand Down
2 changes: 1 addition & 1 deletion Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
}
}

Expand Down
2 changes: 1 addition & 1 deletion Coder-Desktop/Coder-DesktopTests/AgentsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
36 changes: 30 additions & 6 deletions Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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
Expand Down
Loading