- Notifications
You must be signed in to change notification settings - Fork5
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
dc8330356de38db161b050d54e6f31f725e72421069eb598f919160fFile 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() } | ||
MemberAuthor 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.