- Notifications
You must be signed in to change notification settings - Fork3
feat: add workspace apps#136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
dc83303
56de38d
b161b05
0d54e6f
31f725e
7242106
9eb598f
919160f
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
import FluidMenuBarExtra | ||
import NetworkExtension | ||
import SDWebImageSVGCoder | ||
import SDWebImageSwiftUI | ||
import SwiftUI | ||
import VPNLib | ||
@@ -66,6 +68,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { | ||
} | ||
func applicationDidFinishLaunching(_: Notification) { | ||
// Init SVG loader | ||
SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared) | ||
MemberAuthor
| ||
menuBar = .init(menuBarExtra: FluidMenuBarExtra( | ||
title: "Coder Desktop", | ||
image: "MenuBarIcon", | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
import CoderSDK | ||
import os | ||
import SwiftUI | ||
// Each row in the workspaces list is an agent or an offline workspace | ||
@@ -26,6 +28,13 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable { | ||
} | ||
} | ||
var workspaceID: UUID { | ||
switch self { | ||
case let .agent(agent): agent.wsID | ||
case let .offlineWorkspace(workspace): workspace.id | ||
} | ||
} | ||
static func < (lhs: VPNMenuItem, rhs: VPNMenuItem) -> Bool { | ||
switch (lhs, rhs) { | ||
case let (.agent(lhsAgent), .agent(rhsAgent)): | ||
@@ -44,11 +53,17 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable { | ||
struct MenuItemView: View { | ||
@EnvironmentObject var state: AppState | ||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "VPNMenu") | ||
let item: VPNMenuItem | ||
let baseAccessURL: URL | ||
@State private var nameIsSelected: Bool = false | ||
@State private var copyIsSelected: Bool = false | ||
private let defaultVisibleApps = 5 | ||
@State private var apps: [WorkspaceApp] = [] | ||
private var itemName: AttributedString { | ||
let name = switch item { | ||
case let .agent(agent): agent.primaryHost ?? "\(item.wsName).\(state.hostnameSuffix)" | ||
@@ -70,37 +85,90 @@ struct MenuItemView: View { | ||
} | ||
var body: some View { | ||
VStack(spacing: 0) { | ||
HStack(spacing: 0) { | ||
Link(destination: wsURL) { | ||
HStack(spacing: Theme.Size.trayPadding) { | ||
StatusDot(color: item.status.color) | ||
Text(itemName).lineLimit(1).truncationMode(.tail) | ||
Spacer() | ||
}.padding(.horizontal, Theme.Size.trayPadding) | ||
.frame(minHeight: 22) | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
.foregroundStyle(nameIsSelected ? .white : .primary) | ||
.background(nameIsSelected ? Color.accentColor.opacity(0.8) : .clear) | ||
.clipShape(.rect(cornerRadius: Theme.Size.rectCornerRadius)) | ||
.onHoverWithPointingHand { hovering in | ||
nameIsSelected = hovering | ||
} | ||
Spacer() | ||
}.buttonStyle(.plain) | ||
if case let .agent(agent) = item, let copyableDNS = agent.primaryHost { | ||
Button { | ||
NSPasteboard.general.clearContents() | ||
NSPasteboard.general.setString(copyableDNS, forType: .string) | ||
} label: { | ||
Image(systemName: "doc.on.doc") | ||
.symbolVariant(.fill) | ||
.padding(3) | ||
.contentShape(Rectangle()) | ||
}.foregroundStyle(copyIsSelected ? .white : .primary) | ||
.imageScale(.small) | ||
.background(copyIsSelected ? Color.accentColor.opacity(0.8) : .clear) | ||
.clipShape(.rect(cornerRadius: Theme.Size.rectCornerRadius)) | ||
.onHoverWithPointingHand { hovering in copyIsSelected = hovering } | ||
.buttonStyle(.plain) | ||
.padding(.trailing, Theme.Size.trayMargin) | ||
} | ||
} | ||
if !apps.isEmpty { | ||
HStack(spacing: 17) { | ||
ForEach(apps.prefix(defaultVisibleApps), id: \.id) { app in | ||
WorkspaceAppIcon(app: app) | ||
ethanndickson marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
.frame(width: Theme.Size.appIconWidth, height: Theme.Size.appIconHeight) | ||
} | ||
if apps.count < defaultVisibleApps { | ||
Spacer() | ||
} | ||
} | ||
.padding(.leading, apps.count < defaultVisibleApps ? 14 : 0) | ||
.padding(.bottom, 5) | ||
.padding(.top, 10) | ||
} | ||
} | ||
.task { await loadApps() } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more.
| ||
} | ||
func loadApps() async { | ||
// If this menu item is an agent, and the user is logged in | ||
if case let .agent(agent) = item, | ||
let client = state.client, | ||
let host = agent.primaryHost, | ||
let baseAccessURL = state.baseAccessURL, | ||
// Like the CLI, we'll re-use the existing session token to populate the URL | ||
let sessionToken = state.sessionToken | ||
{ | ||
let workspace: CoderSDK.Workspace | ||
do { | ||
workspace = try await retry(floor: .milliseconds(100), ceil: .seconds(10)) { | ||
do { | ||
return try await client.workspace(item.workspaceID) | ||
} catch { | ||
logger.error("Failed to load apps for workspace \(item.wsName): \(error.localizedDescription)") | ||
throw error | ||
} | ||
} | ||
} catch { return } // Task cancelled | ||
if let wsAgent = workspace | ||
.latest_build.resources | ||
.compactMap(\.agents) | ||
.flatMap(\.self) | ||
.first(where: { $0.id == agent.id }) | ||
{ | ||
apps = agentToApps(logger, wsAgent, host, baseAccessURL, sessionToken) | ||
} else { | ||
logger.error("Could not find agent '\(agent.id)' in workspace '\(item.wsName)' resources") | ||
} | ||
} | ||
} | ||
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.