Skip to content

Commit a00e365

Browse files
committed
feat: add workspace apps
1 parent 33da515 commit a00e365

File tree

10 files changed

+649
-37
lines changed

10 files changed

+649
-37
lines changed

Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import FluidMenuBarExtra
22
import NetworkExtension
3+
import SDWebImageSVGCoder
4+
import SDWebImageSwiftUI
35
import SwiftUI
46
import VPNLib
57

@@ -66,6 +68,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
6668
}
6769

6870
func applicationDidFinishLaunching(_: Notification) {
71+
// Init SVG loader
72+
SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
73+
6974
menuBar = .init(menuBarExtra: FluidMenuBarExtra(
7075
title: "Coder Desktop",
7176
image: "MenuBarIcon",

Coder-Desktop/Coder-Desktop/State.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class AppState: ObservableObject {
3737
}
3838
}
3939

40-
private var client: Client?
40+
public var client: Client?
4141

4242
@Published var useLiteralHeaders: Bool = UserDefaults.standard.bool(forKey: Keys.useLiteralHeaders) {
4343
didSet {

Coder-Desktop/Coder-Desktop/Theme.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ enum Theme {
77
static let trayInset: CGFloat = trayMargin + trayPadding
88

99
static let rectCornerRadius: CGFloat = 4
10+
11+
static let appIconWidth: CGFloat = 30
12+
static let appIconHeight: CGFloat = 30
13+
static let appIconSize: CGSize = .init(width: appIconWidth, height: appIconHeight)
1014
}
1115

1216
static let defaultVisibleAgents = 5

Coder-Desktop/Coder-Desktop/Views/ResponsiveLink.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,8 @@ struct ResponsiveLink: View {
1313
.font(.subheadline)
1414
.foregroundColor(isPressed ? .red : .blue)
1515
.underline(isHovered, color: isPressed ? .red : .blue)
16-
.onHover { hovering in
16+
.onHoverWithPointingHand { hovering in
1717
isHovered = hovering
18-
if hovering {
19-
NSCursor.pointingHand.push()
20-
} else {
21-
NSCursor.pop()
22-
}
2318
}
2419
.simultaneousGesture(
2520
DragGesture(minimumDistance: 0)

Coder-Desktop/Coder-Desktop/Views/Util.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,16 @@ extension UUID {
3131
self.init(uuid: uuid)
3232
}
3333
}
34+
35+
public extension View {
36+
@inlinable nonisolated func onHoverWithPointingHand(perform action: @escaping (Bool) -> Void) -> some View {
37+
onHover { hovering in
38+
if hovering {
39+
NSCursor.pointingHand.push()
40+
} else {
41+
NSCursor.pop()
42+
}
43+
action(hovering)
44+
}
45+
}
46+
}

Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift

Lines changed: 98 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import CoderSDK
2+
import os
13
import SwiftUI
24

35
// Each row in the workspaces list is an agent or an offline workspace
@@ -26,6 +28,13 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable {
2628
}
2729
}
2830

31+
var workspaceID: UUID {
32+
switch self {
33+
case let .agent(agent): agent.wsID
34+
case let .offlineWorkspace(workspace): workspace.id
35+
}
36+
}
37+
2938
static func < (lhs: VPNMenuItem, rhs: VPNMenuItem) -> Bool {
3039
switch (lhs, rhs) {
3140
case let (.agent(lhsAgent), .agent(rhsAgent)):
@@ -44,11 +53,17 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable {
4453
struct MenuItemView: View {
4554
@EnvironmentObject var state: AppState
4655

56+
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "VPNMenu")
57+
4758
let item: VPNMenuItem
4859
let baseAccessURL: URL
60+
4961
@State private var nameIsSelected: Bool = false
5062
@State private var copyIsSelected: Bool = false
5163

64+
private let defaultVisibleApps = 5
65+
@State private var apps: [WorkspaceApp] = []
66+
5267
private var itemName: AttributedString {
5368
let name = switch item {
5469
case let .agent(agent): agent.primaryHost ?? "\(item.wsName).\(state.hostnameSuffix)"
@@ -70,37 +85,90 @@ struct MenuItemView: View {
7085
}
7186

7287
var body: some View {
73-
HStack(spacing: 0) {
74-
Link(destination: wsURL) {
75-
HStack(spacing: Theme.Size.trayPadding) {
76-
StatusDot(color: item.status.color)
77-
Text(itemName).lineLimit(1).truncationMode(.tail)
88+
VStack(spacing: 0) {
89+
HStack(spacing: 0) {
90+
Link(destination: wsURL) {
91+
HStack(spacing: Theme.Size.trayPadding) {
92+
StatusDot(color: item.status.color)
93+
Text(itemName).lineLimit(1).truncationMode(.tail)
94+
Spacer()
95+
}.padding(.horizontal, Theme.Size.trayPadding)
96+
.frame(minHeight: 22)
97+
.frame(maxWidth: .infinity, alignment: .leading)
98+
.foregroundStyle(nameIsSelected ? .white : .primary)
99+
.background(nameIsSelected ? Color.accentColor.opacity(0.8) : .clear)
100+
.clipShape(.rect(cornerRadius: Theme.Size.rectCornerRadius))
101+
.onHoverWithPointingHand { hovering in
102+
nameIsSelected = hovering
103+
}
78104
Spacer()
79-
}.padding(.horizontal, Theme.Size.trayPadding)
80-
.frame(minHeight: 22)
81-
.frame(maxWidth: .infinity, alignment: .leading)
82-
.foregroundStyle(nameIsSelected ? .white : .primary)
83-
.background(nameIsSelected ? Color.accentColor.opacity(0.8) : .clear)
84-
.clipShape(.rect(cornerRadius: Theme.Size.rectCornerRadius))
85-
.onHover { hovering in nameIsSelected = hovering }
86-
Spacer()
87-
}.buttonStyle(.plain)
88-
if case let .agent(agent) = item, let copyableDNS = agent.primaryHost {
89-
Button {
90-
NSPasteboard.general.clearContents()
91-
NSPasteboard.general.setString(copyableDNS, forType: .string)
92-
} label: {
93-
Image(systemName: "doc.on.doc")
94-
.symbolVariant(.fill)
95-
.padding(3)
96-
.contentShape(Rectangle())
97-
}.foregroundStyle(copyIsSelected ? .white : .primary)
98-
.imageScale(.small)
99-
.background(copyIsSelected ? Color.accentColor.opacity(0.8) : .clear)
100-
.clipShape(.rect(cornerRadius: Theme.Size.rectCornerRadius))
101-
.onHover { hovering in copyIsSelected = hovering }
102-
.buttonStyle(.plain)
103-
.padding(.trailing, Theme.Size.trayMargin)
105+
}.buttonStyle(.plain)
106+
if case let .agent(agent) = item, let copyableDNS = agent.primaryHost {
107+
Button {
108+
NSPasteboard.general.clearContents()
109+
NSPasteboard.general.setString(copyableDNS, forType: .string)
110+
} label: {
111+
Image(systemName: "doc.on.doc")
112+
.symbolVariant(.fill)
113+
.padding(3)
114+
.contentShape(Rectangle())
115+
}.foregroundStyle(copyIsSelected ? .white : .primary)
116+
.imageScale(.small)
117+
.background(copyIsSelected ? Color.accentColor.opacity(0.8) : .clear)
118+
.clipShape(.rect(cornerRadius: Theme.Size.rectCornerRadius))
119+
.onHoverWithPointingHand { hovering in copyIsSelected = hovering }
120+
.buttonStyle(.plain)
121+
.padding(.trailing, Theme.Size.trayMargin)
122+
}
123+
}
124+
if !apps.isEmpty {
125+
HStack(spacing: 17) {
126+
ForEach(apps.prefix(defaultVisibleApps), id: \.id) { app in
127+
WorkspaceAppIcon(app: app)
128+
.frame(width: Theme.Size.appIconWidth, height: Theme.Size.appIconHeight)
129+
}
130+
if apps.count < defaultVisibleApps {
131+
Spacer()
132+
}
133+
}
134+
.padding(.leading, apps.count < defaultVisibleApps ? 14 : 0)
135+
.padding(.bottom, 5)
136+
.padding(.top, 10)
137+
}
138+
}
139+
.task { await loadApps() }
140+
}
141+
142+
func loadApps() async {
143+
// If this menu item is an agent, and the user is logged in
144+
if case let .agent(agent) = item,
145+
let client = state.client,
146+
let host = agent.primaryHost,
147+
let baseAccessURL = state.baseAccessURL,
148+
// Like the CLI, we'll re-use the existing session token to populate the URL
149+
let sessionToken = state.sessionToken
150+
{
151+
let workspace: CoderSDK.Workspace
152+
do {
153+
workspace = try await retry(floor: .milliseconds(100), ceil: .seconds(10)) {
154+
do {
155+
return try await client.workspace(item.workspaceID)
156+
} catch {
157+
logger.error("Failed to load apps for workspace \(item.wsName): \(error.localizedDescription)")
158+
throw error
159+
}
160+
}
161+
} catch { return } // Task cancelled
162+
163+
if let wsAgent = workspace
164+
.latest_build.resources
165+
.compactMap(\.agents)
166+
.flatMap(\.self)
167+
.first(where: { $0.id == agent.id })
168+
{
169+
apps = agentToApps(logger, wsAgent, host, baseAccessURL, sessionToken)
170+
} else {
171+
logger.error("Could not find agent '\(agent.id)' in workspace '\(item.wsName)' resources")
104172
}
105173
}
106174
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy