Certificate UI/Import (#798)

* Sketching out.

* WIP

* WIP

* Dump

* Apply stash

* Merge + WIP

* UI

* More WIP

* Agent config

* UI cleanup

* Restore dirty files

* XPC

* Edit/delete

* UI fixes

* Cleanup

* Change id for OpenSSHCertificate to hex of md5

* Fix runtime warning for confirmation dialog

* Mark strings as reviewed

* Cleanup

* Fix agent tests
This commit is contained in:
Max Goedjen
2026-05-06 01:03:21 -07:00
committed by GitHub
parent 2f4d10d70d
commit b337b24641
35 changed files with 1516 additions and 225 deletions

View File

@@ -1,25 +1,32 @@
import SwiftUI
import SecretKit
import SSHProtocolKit
struct StoreListView: View {
@Binding var activeSecret: AnySecret?
enum StoreListSelection: Hashable {
case secret(AnySecret)
case certificate(OpenSSHCertificate)
}
@Binding var selection: StoreListSelection?
@Environment(\.secretStoreList) private var storeList
@Environment(\.certificateStore) private var certificateStore
private func secretDeleted(secret: AnySecret) {
activeSecret = nextDefaultSecret
selection = nextDefaultSecret.map(StoreListSelection.secret)
}
private func secretRenamed(secret: AnySecret) {
// Pull new version from store, so we get all updated attributes
activeSecret = nil
activeSecret = storeList.allSecrets.first(where: { $0.id == secret.id })
selection = nil
selection = storeList.allSecrets.first(where: { $0.id == secret.id }).map(StoreListSelection.secret)
}
var body: some View {
NavigationSplitView {
List(selection: $activeSecret) {
List(selection: $selection) {
ForEach(storeList.stores) { store in
if store.isAvailable {
Section(header: Text(store.name)) {
@@ -30,29 +37,51 @@ struct StoreListView: View {
deletedSecret: secretDeleted,
renamedSecret: secretRenamed,
)
.tag(StoreListSelection.secret(secret))
}
}
}
}
if !certificateStore.certificates.isEmpty {
Section("Certificates") {
ForEach(certificateStore.certificates) { certificate in
CertificateListItemView(
certificate: certificate,
deletedCertificate: { _ in },
renamedCertificate: { _ in }
)
.tag(StoreListSelection.certificate(certificate))
}
}
}
}
} detail: {
if let activeSecret {
SecretDetailView(secret: activeSecret)
} else if let nextDefaultSecret {
// This just means onAppear hasn't executed yet.
// Do this to avoid a blip.
SecretDetailView(secret: nextDefaultSecret)
} else {
if let modifiable = storeList.modifiableStore, modifiable.isAvailable {
EmptyStoreView(store: modifiable)
switch selection {
case .secret(let secret):
SecretDetailView(secret: secret, certificates: certificateStore.certificates(for: secret)) {
selection = .certificate($0)
}
case .certificate(let certificate):
CertificateDetailView(certificate: certificate)
case nil:
if let nextDefaultSecret {
// This just means onAppear hasn't executed yet.
// Do this to avoid a blip.
SecretDetailView(secret: nextDefaultSecret, certificates: certificateStore.certificates(for: nextDefaultSecret)) {
selection = .certificate($0)
}
} else {
EmptyStoreView(store: storeList.stores.first(where: \.isAvailable))
if let modifiable = storeList.modifiableStore, modifiable.isAvailable {
EmptyStoreView(store: modifiable)
} else {
EmptyStoreView(store: storeList.stores.first(where: \.isAvailable))
}
}
}
}
.navigationSplitViewStyle(.balanced)
.onAppear {
activeSecret = nextDefaultSecret
selection = nextDefaultSecret.map(StoreListSelection.secret)
}
.frame(minWidth: 100, idealWidth: 240)